tenets.core.analysis¶
tenets.core.analysis¶
Analysis package.
Re-exports the main CodeAnalyzer after directory reorganization.
This module intentionally re-exports CodeAnalyzer so callers can import tenets.core.analysis.CodeAnalyzer. The implementation lives in analyzer.py and does not import this package-level module, so exposing the symbol here will not create a circular import.
base¶
Base abstract class for language-specific code analyzers.
This module provides the abstract base class that all language-specific analyzers must implement. It defines the common interface for extracting imports, exports, structure, and calculating complexity metrics.
LanguageAnalyzer¶
Bases: ABC
Abstract base class for language-specific analyzers.
Each language analyzer must implement this interface to provide language-specific analysis capabilities. This ensures a consistent API across all language analyzers while allowing for language-specific implementation details.
| ATTRIBUTE | DESCRIPTION |
|---|---|
language_name | Name of the programming language TYPE: |
file_extensions | List of file extensions this analyzer handles |
entry_points | Common entry point filenames for this language |
project_indicators | Framework/project type indicators |
extract_importsabstractmethod¶
Extract import statements from source code.
This method should identify and extract all import/include/require statements from the source code, including their type, location, and whether they are relative imports.
| PARAMETER | DESCRIPTION |
|---|---|
content | Source code content as string TYPE: |
file_path | Path to the file being analyzed TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
List[ImportInfo] | List of ImportInfo objects containing: - module: The imported module/package name - alias: Any alias assigned to the import - line: Line number of the import - type: Type of import (e.g., 'import', 'from', 'require') - is_relative: Whether this is a relative import - Additional language-specific fields |
Examples:
Source code in tenets/core/analysis/base.py
@abstractmethod
def extract_imports(self, content: str, file_path: Path) -> List[ImportInfo]:
"""Extract import statements from source code.
This method should identify and extract all import/include/require
statements from the source code, including their type, location,
and whether they are relative imports.
Args:
content: Source code content as string
file_path: Path to the file being analyzed
Returns:
List of ImportInfo objects containing:
- module: The imported module/package name
- alias: Any alias assigned to the import
- line: Line number of the import
- type: Type of import (e.g., 'import', 'from', 'require')
- is_relative: Whether this is a relative import
- Additional language-specific fields
Examples:
Python: import os, from datetime import datetime
JavaScript: import React from 'react', const fs = require('fs')
Go: import "fmt", import _ "database/sql"
"""
pass
extract_exportsabstractmethod¶
Extract exported symbols from source code.
This method should identify all symbols (functions, classes, variables) that are exported from the module and available for use by other modules.
| PARAMETER | DESCRIPTION |
|---|---|
content | Source code content as string TYPE: |
file_path | Path to the file being analyzed TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
List[Dict[str, Any]] | List of dictionaries containing: - name: Name of the exported symbol - type: Type of export (e.g., 'function', 'class', 'variable') - line: Line number where the export is defined - Additional language-specific metadata |
Examples:
Source code in tenets/core/analysis/base.py
@abstractmethod
def extract_exports(self, content: str, file_path: Path) -> List[Dict[str, Any]]:
"""Extract exported symbols from source code.
This method should identify all symbols (functions, classes, variables)
that are exported from the module and available for use by other modules.
Args:
content: Source code content as string
file_path: Path to the file being analyzed
Returns:
List of dictionaries containing:
- name: Name of the exported symbol
- type: Type of export (e.g., 'function', 'class', 'variable')
- line: Line number where the export is defined
- Additional language-specific metadata
Examples:
Python: __all__ = ['func1', 'Class1'], public functions/classes
JavaScript: export default App, export { util1, util2 }
Go: Capitalized functions/types are exported
"""
pass
extract_structureabstractmethod¶
Extract code structure from source file.
This method should parse the source code and extract structural elements like classes, functions, methods, variables, constants, and other language-specific constructs.
| PARAMETER | DESCRIPTION |
|---|---|
content | Source code content as string TYPE: |
file_path | Path to the file being analyzed TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
CodeStructure | CodeStructure object containing: - classes: List of ClassInfo objects - functions: List of FunctionInfo objects - variables: List of variable definitions - constants: List of constant definitions - interfaces: List of interface definitions (if applicable) - Additional language-specific structures |
Note
The depth of extraction depends on the language's parsing capabilities. AST-based parsing provides more detail than regex-based parsing.
Source code in tenets/core/analysis/base.py
@abstractmethod
def extract_structure(self, content: str, file_path: Path) -> CodeStructure:
"""Extract code structure from source file.
This method should parse the source code and extract structural
elements like classes, functions, methods, variables, constants,
and other language-specific constructs.
Args:
content: Source code content as string
file_path: Path to the file being analyzed
Returns:
CodeStructure object containing:
- classes: List of ClassInfo objects
- functions: List of FunctionInfo objects
- variables: List of variable definitions
- constants: List of constant definitions
- interfaces: List of interface definitions (if applicable)
- Additional language-specific structures
Note:
The depth of extraction depends on the language's parsing
capabilities. AST-based parsing provides more detail than
regex-based parsing.
"""
pass
calculate_complexityabstractmethod¶
Calculate complexity metrics for the source code.
This method should calculate various complexity metrics including cyclomatic complexity, cognitive complexity, and other relevant metrics for understanding code complexity and maintainability.
| PARAMETER | DESCRIPTION |
|---|---|
content | Source code content as string TYPE: |
file_path | Path to the file being analyzed TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
ComplexityMetrics | ComplexityMetrics object containing: - cyclomatic: McCabe cyclomatic complexity - cognitive: Cognitive complexity score - halstead: Halstead complexity metrics (if calculated) - line_count: Total number of lines - function_count: Number of functions/methods - class_count: Number of classes - max_depth: Maximum nesting depth - maintainability_index: Maintainability index score - Additional language-specific metrics |
Complexity Calculation
Source code in tenets/core/analysis/base.py
@abstractmethod
def calculate_complexity(self, content: str, file_path: Path) -> ComplexityMetrics:
"""Calculate complexity metrics for the source code.
This method should calculate various complexity metrics including
cyclomatic complexity, cognitive complexity, and other relevant
metrics for understanding code complexity and maintainability.
Args:
content: Source code content as string
file_path: Path to the file being analyzed
Returns:
ComplexityMetrics object containing:
- cyclomatic: McCabe cyclomatic complexity
- cognitive: Cognitive complexity score
- halstead: Halstead complexity metrics (if calculated)
- line_count: Total number of lines
- function_count: Number of functions/methods
- class_count: Number of classes
- max_depth: Maximum nesting depth
- maintainability_index: Maintainability index score
- Additional language-specific metrics
Complexity Calculation:
Cyclomatic: Number of linearly independent paths
Cognitive: Measure of how difficult code is to understand
Halstead: Based on operators and operands count
"""
pass
analyze¶
Run complete analysis on source file.
This method orchestrates all analysis methods to provide a complete analysis of the source file. It can be overridden by specific analyzers if they need custom orchestration logic.
| PARAMETER | DESCRIPTION |
|---|---|
content | Source code content as string TYPE: |
file_path | Path to the file being analyzed TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
Dict[str, Any] | Dictionary containing all analysis results: - imports: List of ImportInfo objects - exports: List of export dictionaries - structure: CodeStructure object - complexity: ComplexityMetrics object - Additional analysis results |
Note
Subclasses can override this method to add language-specific analysis steps or modify the analysis pipeline.
Source code in tenets/core/analysis/base.py
def analyze(self, content: str, file_path: Path) -> Dict[str, Any]:
"""Run complete analysis on source file.
This method orchestrates all analysis methods to provide a complete
analysis of the source file. It can be overridden by specific
analyzers if they need custom orchestration logic.
Args:
content: Source code content as string
file_path: Path to the file being analyzed
Returns:
Dictionary containing all analysis results:
- imports: List of ImportInfo objects
- exports: List of export dictionaries
- structure: CodeStructure object
- complexity: ComplexityMetrics object
- Additional analysis results
Note:
Subclasses can override this method to add language-specific
analysis steps or modify the analysis pipeline.
"""
return {
"imports": self.extract_imports(content, file_path),
"exports": self.extract_exports(content, file_path),
"structure": self.extract_structure(content, file_path),
"complexity": self.calculate_complexity(content, file_path),
}
supports_file¶
get_language_info¶
Get information about the language this analyzer supports.
| RETURNS | DESCRIPTION |
|---|---|
Dict[str, Any] | Dictionary containing: - name: Language name - extensions: Supported file extensions - features: List of supported analysis features |
Source code in tenets/core/analysis/base.py
def get_language_info(self) -> Dict[str, Any]:
"""Get information about the language this analyzer supports.
Returns:
Dictionary containing:
- name: Language name
- extensions: Supported file extensions
- features: List of supported analysis features
"""
return {
"name": self.language_name,
"extensions": self.file_extensions,
"features": ["imports", "exports", "structure", "complexity"],
}
project_detector¶
Project type detection and entry point discovery.
This module provides intelligent detection of project types, main entry points, and project structure based on language analyzers and file patterns.
ProjectDetector¶
Detects project type and structure using language analyzers.
This class leverages the language-specific analyzers to detect project types and entry points, avoiding duplication of language-specific knowledge.
Initialize project detector with language analyzers.
Source code in tenets/core/analysis/project_detector.py
def __init__(self):
"""Initialize project detector with language analyzers."""
self.logger = get_logger(__name__)
# Initialize all language analyzers
self.analyzers = [
PythonAnalyzer(),
JavaScriptAnalyzer(),
JavaAnalyzer(),
GoAnalyzer(),
RustAnalyzer(),
CppAnalyzer(),
CSharpAnalyzer(),
RubyAnalyzer(),
PhpAnalyzer(),
SwiftAnalyzer(),
KotlinAnalyzer(),
ScalaAnalyzer(),
DartAnalyzer(),
GDScriptAnalyzer(),
HTMLAnalyzer(),
CSSAnalyzer(),
]
# Build dynamic mappings from analyzers
self._build_mappings()
# Additional framework patterns not tied to specific languages
self.FRAMEWORK_PATTERNS = {
"docker": ["Dockerfile", "docker-compose.yml", "docker-compose.yaml"],
"kubernetes": ["k8s/", "kubernetes/", "deployment.yaml", "service.yaml"],
"terraform": ["*.tf", "terraform.tfvars"],
"ansible": ["ansible.cfg", "playbook.yml", "inventory"],
"ci_cd": [".github/workflows/", ".gitlab-ci.yml", "Jenkinsfile", ".travis.yml"],
}
detect_project_type¶
Detect project type and main entry points.
| PARAMETER | DESCRIPTION |
|---|---|
path | Root directory to analyze TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
Dict[str, any] | Dictionary containing: - type: Primary project type - languages: List of detected languages - frameworks: List of detected frameworks - entry_points: List of likely entry point files - confidence: Confidence score (0-1) |
Source code in tenets/core/analysis/project_detector.py
def detect_project_type(self, path: Path) -> Dict[str, any]:
"""Detect project type and main entry points.
Args:
path: Root directory to analyze
Returns:
Dictionary containing:
- type: Primary project type
- languages: List of detected languages
- frameworks: List of detected frameworks
- entry_points: List of likely entry point files
- confidence: Confidence score (0-1)
"""
path = Path(path)
if not path.exists():
return {
"type": "unknown",
"languages": [],
"frameworks": [],
"entry_points": [],
"confidence": 0.0,
}
# Collect all files
all_files = []
for ext in ["*.*", "Dockerfile", "Makefile", "Jenkinsfile"]:
all_files.extend(path.rglob(ext))
# Analyze file extensions to detect languages
extensions = Counter()
for file in all_files:
if file.is_file():
ext = file.suffix.lower()
if ext:
extensions[ext] += 1
# Determine primary languages based on extensions
languages = []
for ext, count in extensions.most_common(10):
if ext in self.EXTENSION_TO_LANGUAGE:
lang = self.EXTENSION_TO_LANGUAGE[ext]
if lang not in languages:
languages.append(lang)
# Detect frameworks based on indicators
frameworks = []
file_names = {f.name for f in all_files if f.is_file()}
dir_names = {f.name for f in all_files if f.is_dir()}
# Check language-specific project indicators
for project_type, indicators in self.PROJECT_INDICATORS.items():
for indicator in indicators:
if indicator in file_names or indicator in dir_names:
frameworks.append(project_type)
break
# Check general framework patterns
for framework, patterns in self.FRAMEWORK_PATTERNS.items():
for pattern in patterns:
if pattern.endswith("/"):
# Directory pattern
if pattern[:-1] in dir_names:
frameworks.append(framework)
break
elif "*" in pattern:
# Glob pattern
if any(f.match(pattern) for f in all_files if f.is_file()):
frameworks.append(framework)
break
else:
# File pattern
if pattern in file_names:
frameworks.append(framework)
break
# Find entry points
entry_points = self._find_entry_points(path, languages, file_names)
# Determine primary project type
project_type = self._determine_project_type(languages, frameworks)
# Calculate confidence
confidence = self._calculate_confidence(languages, frameworks, entry_points)
return {
"type": project_type,
"languages": languages[:3], # Top 3 languages
"frameworks": list(set(frameworks))[:3], # Top 3 unique frameworks
"entry_points": entry_points[:5], # Top 5 entry points
"confidence": confidence,
}
find_main_file¶
Find the most likely main/entry file in a project.
| PARAMETER | DESCRIPTION |
|---|---|
path | Directory to search in TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
Optional[Path] | Path to the main file, or None if not found |
Source code in tenets/core/analysis/project_detector.py
def find_main_file(self, path: Path) -> Optional[Path]:
"""Find the most likely main/entry file in a project.
Args:
path: Directory to search in
Returns:
Path to the main file, or None if not found
"""
path = Path(path)
if not path.is_dir():
return None
# Detect project info
project_info = self.detect_project_type(path)
# Use detected entry points
if project_info["entry_points"]:
main_file = path / project_info["entry_points"][0]
if main_file.exists():
return main_file
# Fall back to language-specific patterns
for lang in project_info["languages"]:
if lang in self.ENTRY_POINTS:
for entry_point in self.ENTRY_POINTS[lang]:
for file_path in path.rglob(entry_point):
if file_path.is_file():
return file_path
return None
analyzer¶
Main code analyzer orchestrator for Tenets.
This module coordinates language-specific analyzers and provides a unified interface for analyzing source code files. It handles analyzer selection, caching, parallel processing, and fallback strategies.
CodeAnalyzer¶
Main code analysis orchestrator.
Coordinates language-specific analyzers and provides a unified interface for analyzing source code files. Handles caching, parallel processing, analyzer selection, and fallback strategies.
| ATTRIBUTE | DESCRIPTION |
|---|---|
config | TenetsConfig instance for configuration |
logger | Logger instance for logging |
cache | AnalysisCache for caching analysis results |
analyzers | Dictionary mapping file extensions to analyzer instances |
stats | Analysis statistics and metrics |
Initialize the code analyzer.
| PARAMETER | DESCRIPTION |
|---|---|
config | Tenets configuration object TYPE: |
Source code in tenets/core/analysis/analyzer.py
def __init__(self, config: TenetsConfig):
"""Initialize the code analyzer.
Args:
config: Tenets configuration object
"""
self.config = config
self.logger = get_logger(__name__)
# Initialize cache if enabled
self.cache = None
if config.cache.enabled:
self.cache = AnalysisCache(config.cache.directory)
self.logger.info(f"Cache initialized at {config.cache.directory}")
# Initialize language analyzers
self.analyzers = self._initialize_analyzers()
# Thread pool for parallel analysis
self._executor = concurrent.futures.ThreadPoolExecutor(max_workers=config.scanner.workers)
# Analysis statistics
self.stats = {
"files_analyzed": 0,
"cache_hits": 0,
"cache_misses": 0,
"errors": 0,
"total_time": 0,
"languages": {},
}
self.logger.info(f"CodeAnalyzer initialized with {len(self.analyzers)} language analyzers")
analyze_file¶
analyze_file(file_path: Path, deep: bool = False, extract_keywords: bool = True, use_cache: bool = True, progress_callback: Optional[Callable] = None) -> FileAnalysis
Analyze a single file.
Performs language-specific analysis on a file, extracting imports, structure, complexity metrics, and other relevant information.
| PARAMETER | DESCRIPTION |
|---|---|
file_path | Path to the file to analyze TYPE: |
deep | Whether to perform deep analysis (AST parsing, etc.) TYPE: |
extract_keywords | Whether to extract keywords from content TYPE: |
use_cache | Whether to use cached results if available TYPE: |
progress_callback | Optional callback for progress updates |
| RETURNS | DESCRIPTION |
|---|---|
FileAnalysis | FileAnalysis object with complete analysis results |
| RAISES | DESCRIPTION |
|---|---|
FileNotFoundError | If file doesn't exist |
PermissionError | If file cannot be read |
Source code in tenets/core/analysis/analyzer.py
def analyze_file(
self,
file_path: Path,
deep: bool = False,
extract_keywords: bool = True,
use_cache: bool = True,
progress_callback: Optional[Callable] = None,
) -> FileAnalysis:
"""Analyze a single file.
Performs language-specific analysis on a file, extracting imports,
structure, complexity metrics, and other relevant information.
Args:
file_path: Path to the file to analyze
deep: Whether to perform deep analysis (AST parsing, etc.)
extract_keywords: Whether to extract keywords from content
use_cache: Whether to use cached results if available
progress_callback: Optional callback for progress updates
Returns:
FileAnalysis object with complete analysis results
Raises:
FileNotFoundError: If file doesn't exist
PermissionError: If file cannot be read
"""
file_path = Path(file_path)
# Check cache first
if use_cache and self.cache:
cached_analysis = self.cache.get_file_analysis(file_path)
if cached_analysis:
self.stats["cache_hits"] += 1
self.logger.debug(f"Cache hit for {file_path}")
if progress_callback:
progress_callback("cache_hit", file_path)
return cached_analysis
else:
self.stats["cache_misses"] += 1
self.logger.debug(f"Analyzing file: {file_path}")
try:
# Read file content
content = self._read_file_content(file_path)
# Create base analysis
analysis = FileAnalysis(
path=str(file_path),
content=content,
size=file_path.stat().st_size,
lines=content.count("\n") + 1,
language=self._detect_language(file_path),
file_name=file_path.name,
file_extension=file_path.suffix,
last_modified=datetime.fromtimestamp(file_path.stat().st_mtime),
hash=self._calculate_file_hash(content),
)
# Get appropriate analyzer
analyzer = self._get_analyzer(file_path)
if analyzer is None and deep:
analyzer = GenericAnalyzer()
if analyzer and deep:
try:
# Run language-specific analysis
self.logger.debug(f"Running {analyzer.language_name} analyzer on {file_path}")
analysis_results = analyzer.analyze(content, file_path)
# Update analysis object with results
# Collect results
imports = analysis_results.get("imports", [])
analysis.imports = imports
analysis.exports = analysis_results.get("exports", [])
structure = analysis_results.get("structure", CodeStructure())
# Ensure imports are accessible via structure as well for downstream tools
try:
if hasattr(structure, "imports"):
# Only set if empty to respect analyzers that already populate it
if not getattr(structure, "imports", None):
structure.imports = imports
except Exception:
# Be defensive; never fail analysis due to structure syncing
pass
analysis.structure = structure
analysis.complexity = analysis_results.get("complexity", ComplexityMetrics())
# Extract additional information
if analysis.structure:
analysis.classes = analysis.structure.classes
analysis.functions = analysis.structure.functions
analysis.modules = getattr(analysis.structure, "modules", [])
except Exception as e:
self.logger.warning(f"Language-specific analysis failed for {file_path}: {e}")
analysis.error = str(e)
self.stats["errors"] += 1
# Extract keywords if requested
if extract_keywords:
analysis.keywords = self._extract_keywords(content, analysis.language)
# Add code quality metrics
analysis.quality_score = self._calculate_quality_score(analysis)
# Cache the result
if use_cache and self.cache and not analysis.error:
try:
self.cache.put_file_analysis(file_path, analysis)
except Exception as e:
self.logger.debug(f"Failed to write analysis cache for {file_path}: {e}")
analysis.error = "Cache write error"
# Update statistics
self.stats["files_analyzed"] += 1
self.stats["languages"][analysis.language] = (
self.stats["languages"].get(analysis.language, 0) + 1
)
if progress_callback:
progress_callback("analyzed", file_path)
return analysis
except FileNotFoundError:
# Propagate not found to satisfy tests expecting exception
self.logger.error(f"File not found: {file_path}")
raise
except Exception as e:
self.logger.error(f"Failed to analyze {file_path}: {e}")
self.stats["errors"] += 1
return FileAnalysis(
path=str(file_path),
error=str(e),
file_name=file_path.name,
file_extension=file_path.suffix,
)
analyze_files¶
analyze_files(file_paths: list[Path], deep: bool = False, parallel: bool = True, progress_callback: Optional[Callable] = None) -> list[FileAnalysis]
Analyze multiple files.
| PARAMETER | DESCRIPTION |
|---|---|
file_paths | List of file paths to analyze |
deep | Whether to perform deep analysis TYPE: |
parallel | Whether to analyze files in parallel TYPE: |
progress_callback | Optional callback for progress updates |
| RETURNS | DESCRIPTION |
|---|---|
list[FileAnalysis] | List of FileAnalysis objects |
Source code in tenets/core/analysis/analyzer.py
def analyze_files(
self,
file_paths: list[Path],
deep: bool = False,
parallel: bool = True,
progress_callback: Optional[Callable] = None,
) -> list[FileAnalysis]:
"""Analyze multiple files.
Args:
file_paths: List of file paths to analyze
deep: Whether to perform deep analysis
parallel: Whether to analyze files in parallel
progress_callback: Optional callback for progress updates
Returns:
List of FileAnalysis objects
"""
self.logger.info(f"Analyzing {len(file_paths)} files (parallel={parallel})")
if parallel and len(file_paths) > 1:
# Parallel analysis
futures = []
for file_path in file_paths:
future = self._executor.submit(
self.analyze_file, file_path, deep=deep, progress_callback=progress_callback
)
futures.append((future, file_path))
# Collect results
results = []
for future, file_path in futures:
try:
result = future.result(timeout=self.config.scanner.timeout)
results.append(result)
except concurrent.futures.TimeoutError:
self.logger.warning(f"Analysis timeout for {file_path}")
results.append(FileAnalysis(path=str(file_path), error="Analysis timeout"))
except Exception as e:
self.logger.warning(f"Failed to analyze {file_path}: {e}")
results.append(FileAnalysis(path=str(file_path), error=str(e)))
return results
else:
# Sequential analysis
results = []
for i, file_path in enumerate(file_paths):
result = self.analyze_file(file_path, deep=deep)
results.append(result)
if progress_callback:
progress_callback(i + 1, len(file_paths))
return results
analyze_project¶
analyze_project(project_path: Path, patterns: Optional[list[str]] = None, exclude_patterns: Optional[list[str]] = None, deep: bool = True, parallel: bool = True, progress_callback: Optional[Callable] = None) -> ProjectAnalysis
Analyze an entire project.
| PARAMETER | DESCRIPTION |
|---|---|
project_path | Path to the project root TYPE: |
patterns | File patterns to include (e.g., ['.py', '.js']) |
exclude_patterns | File patterns to exclude |
deep | Whether to perform deep analysis TYPE: |
parallel | Whether to analyze files in parallel TYPE: |
progress_callback | Optional callback for progress updates |
| RETURNS | DESCRIPTION |
|---|---|
ProjectAnalysis | ProjectAnalysis object with complete project analysis |
Source code in tenets/core/analysis/analyzer.py
def analyze_project(
self,
project_path: Path,
patterns: Optional[list[str]] = None,
exclude_patterns: Optional[list[str]] = None,
deep: bool = True,
parallel: bool = True,
progress_callback: Optional[Callable] = None,
) -> ProjectAnalysis:
"""Analyze an entire project.
Args:
project_path: Path to the project root
patterns: File patterns to include (e.g., ['*.py', '*.js'])
exclude_patterns: File patterns to exclude
deep: Whether to perform deep analysis
parallel: Whether to analyze files in parallel
progress_callback: Optional callback for progress updates
Returns:
ProjectAnalysis object with complete project analysis
"""
self.logger.info(f"Analyzing project: {project_path}")
# Collect files to analyze
files = self._collect_project_files(project_path, patterns, exclude_patterns)
self.logger.info(f"Found {len(files)} files to analyze")
# Analyze all files
file_analyses = self.analyze_files(
files, deep=deep, parallel=parallel, progress_callback=progress_callback
)
# Build project analysis
project_analysis = ProjectAnalysis(
path=str(project_path),
name=project_path.name,
files=file_analyses,
total_files=len(file_analyses),
analyzed_files=len([f for f in file_analyses if not f.error]),
failed_files=len([f for f in file_analyses if f.error]),
)
# Calculate project-level metrics
self._calculate_project_metrics(project_analysis)
# Build dependency graph
project_analysis.dependency_graph = self._build_dependency_graph(file_analyses)
# Detect project type and framework
project_analysis.project_type = self._detect_project_type(project_path, file_analyses)
project_analysis.frameworks = self._detect_frameworks(file_analyses)
# Generate summary
project_analysis.summary = self._generate_project_summary(project_analysis)
return project_analysis
generate_report¶
generate_report(analysis: Union[FileAnalysis, ProjectAnalysis, list[FileAnalysis]], format: str = 'json', output_path: Optional[Path] = None) -> AnalysisReport
Generate an analysis report.
| PARAMETER | DESCRIPTION |
|---|---|
analysis | Analysis results to report on TYPE: |
format | Report format ('json', 'html', 'markdown', 'csv') TYPE: |
output_path | Optional path to save the report |
| RETURNS | DESCRIPTION |
|---|---|
AnalysisReport | AnalysisReport object |
Source code in tenets/core/analysis/analyzer.py
def generate_report(
self,
analysis: Union[FileAnalysis, ProjectAnalysis, list[FileAnalysis]],
format: str = "json",
output_path: Optional[Path] = None,
) -> AnalysisReport:
"""Generate an analysis report.
Args:
analysis: Analysis results to report on
format: Report format ('json', 'html', 'markdown', 'csv')
output_path: Optional path to save the report
Returns:
AnalysisReport object
"""
self.logger.info(f"Generating {format} report")
report = AnalysisReport(
timestamp=datetime.now(), format=format, statistics=self.stats.copy()
)
# Generate report content based on format
if format == "json":
report.content = self._generate_json_report(analysis)
elif format == "html":
report.content = self._generate_html_report(analysis)
elif format == "markdown":
report.content = self._generate_markdown_report(analysis)
elif format == "csv":
report.content = self._generate_csv_report(analysis)
else:
raise ValueError(f"Unsupported report format: {format}")
# Save report if output path provided
if output_path:
output_path = Path(output_path)
output_path.parent.mkdir(parents=True, exist_ok=True)
if format in ["json", "csv"]:
output_path.write_text(report.content)
else:
output_path.write_text(report.content, encoding="utf-8")
self.logger.info(f"Report saved to {output_path}")
report.output_path = str(output_path)
return report
shutdown¶
Shutdown the analyzer and clean up resources.
implementations¶
Language-specific code analyzers.
This package contains implementations of language analyzers for various programming languages. Each analyzer provides language-specific parsing and analysis capabilities.
Available analyzers: - PythonAnalyzer: Python code analysis with AST parsing - JavaScriptAnalyzer: JavaScript/TypeScript analysis - JavaAnalyzer: Java code analysis - GoAnalyzer: Go language analysis - RustAnalyzer: Rust code analysis - CppAnalyzer: C/C++ code analysis - CSharpAnalyzer: C# code analysis - SwiftAnalyzer: Swift code analysis - RubyAnalyzer: Ruby code analysis - PhpAnalyzer: PHP code analysis - KotlinAnalyzer: Kotlin code analysis - ScalaAnalyzer: Scala code analysis - DartAnalyzer: Dart code analysis - GDScriptAnalyzer: GDScript (Godot) analysis - HTMLAnalyzer: HTML markup analysis - CSSAnalyzer: CSS stylesheet analysis - GenericAnalyzer: Fallback for unsupported languages
CppAnalyzer¶
Bases: LanguageAnalyzer
C/C++ code analyzer.
Provides analysis for C and C++ files including: - Include directive analysis (system and local) - Class, struct, and union extraction - Template analysis - Function and method extraction - Namespace handling - Macro and preprocessor directive analysis - Modern C++ features (auto, lambdas, smart pointers) - STL usage detection - Memory management patterns
Supports both C and C++ with appropriate feature detection.
Initialize the C++ analyzer with logger.
Source code in tenets/core/analysis/implementations/cpp_analyzer.py
extract_imports¶
Extract includes from C/C++ code.
Handles: - System includes: #include
| PARAMETER | DESCRIPTION |
|---|---|
content | C/C++ source code TYPE: |
file_path | Path to the file being analyzed TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
List[ImportInfo] | List of ImportInfo objects representing includes |
Source code in tenets/core/analysis/implementations/cpp_analyzer.py
def extract_imports(self, content: str, file_path: Path) -> List[ImportInfo]:
"""Extract includes from C/C++ code.
Handles:
- System includes: #include <iostream>
- Local includes: #include "myheader.h"
- Conditional includes with #ifdef
- Include guards
Args:
content: C/C++ source code
file_path: Path to the file being analyzed
Returns:
List of ImportInfo objects representing includes
"""
imports = []
lines = content.split("\n")
# Track preprocessor state
ifdef_stack = []
current_condition = True
for i, line in enumerate(lines, 1):
stripped = line.strip()
# Handle preprocessor conditionals
if stripped.startswith("#ifdef") or stripped.startswith("#ifndef"):
condition = stripped.split()[1] if len(stripped.split()) > 1 else ""
ifdef_stack.append(current_condition)
# We'll track all includes regardless of conditionals for analysis
continue
elif stripped.startswith("#if"):
ifdef_stack.append(current_condition)
continue
elif stripped.startswith("#else"):
if ifdef_stack:
current_condition = not current_condition
continue
elif stripped.startswith("#elif"):
continue
elif stripped.startswith("#endif"):
if ifdef_stack:
current_condition = ifdef_stack.pop()
continue
# System includes
system_include = re.match(r"^\s*#\s*include\s*<([^>]+)>", line)
if system_include:
header = system_include.group(1)
imports.append(
ImportInfo(
module=header,
line=i,
type="system",
is_relative=False,
is_stdlib=self._is_stdlib_header(header),
is_stl=self._is_stl_header(header),
conditional=len(ifdef_stack) > 0,
)
)
continue
# Local includes
local_include = re.match(r'^\s*#\s*include\s*"([^"]+)"', line)
if local_include:
header = local_include.group(1)
imports.append(
ImportInfo(
module=header,
line=i,
type="local",
is_relative=True,
is_project_header=True,
conditional=len(ifdef_stack) > 0,
)
)
continue
# Detect include guards
self._detect_include_guards(content, imports)
return imports
extract_exports¶
Extract exported symbols from C/C++ code.
In C/C++, symbols are exported by default unless static. For headers, we extract declarations. For source files, we extract non-static definitions.
| PARAMETER | DESCRIPTION |
|---|---|
content | C/C++ source code TYPE: |
file_path | Path to the file being analyzed TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
List[Dict[str, Any]] | List of exported symbols |
Source code in tenets/core/analysis/implementations/cpp_analyzer.py
def extract_exports(self, content: str, file_path: Path) -> List[Dict[str, Any]]:
"""Extract exported symbols from C/C++ code.
In C/C++, symbols are exported by default unless static.
For headers, we extract declarations. For source files,
we extract non-static definitions.
Args:
content: C/C++ source code
file_path: Path to the file being analyzed
Returns:
List of exported symbols
"""
exports = []
is_header = file_path.suffix in [".h", ".hh", ".hpp", ".hxx", ".h++"]
# Extract namespace if present
namespace = self._extract_namespace(content)
# Non-static functions
func_pattern = r"^(?:template\s*<[^>]*>\s*)?(?!static)(?:(?:inline|extern|virtual|explicit|constexpr)\s+)*(?:[\w\s\*&:<>]+)\s+(\w+)\s*\([^)]*\)(?:\s*const)?(?:\s*noexcept)?(?:\s*override)?(?:\s*final)?(?:\s*=\s*0)?(?:\s*(?:\{|;))"
for match in re.finditer(func_pattern, content, re.MULTILINE):
func_name = match.group(1)
# Filter out keywords
if func_name not in [
"if",
"for",
"while",
"switch",
"return",
"delete",
"new",
"throw",
"catch",
]:
line_content = content[match.start() : match.end()]
before_window = content[max(0, match.start() - 200) : match.start()]
is_tmpl = (
("template" in line_content)
or ("template" in before_window)
or self._is_template_function(content, match.start())
)
exports.append(
{
"name": func_name,
"type": "function",
"line": content[: match.start()].count("\n") + 1,
"namespace": namespace,
"is_inline": "inline" in line_content,
"is_virtual": "virtual" in line_content,
"is_pure_virtual": "= 0" in line_content,
"is_constexpr": "constexpr" in line_content,
"is_template": is_tmpl,
}
)
# Classes and structs (public by default in struct)
class_pattern = r"\b(?:struct|(?<!enum\s)class)\s+(?:__declspec\([^)]+\)\s+)?(\w+)(?:\s*:\s*(?:public|private|protected)\s+[\w:]+)?(?:\s*\{|;)"
for match in re.finditer(class_pattern, content):
class_name = match.group(1)
is_struct = "struct" in match.group(0)
# Find keyword position for accurate template check
inner = match.group(0)
kw = "struct" if "struct" in inner else "class"
kw_pos = match.start() + inner.find(kw)
exports.append(
{
"name": class_name,
"type": "struct" if is_struct else "class",
"line": content[: match.start()].count("\n") + 1,
"namespace": namespace,
"default_visibility": "public" if is_struct else "private",
"is_template": self._is_template_class(content, kw_pos),
}
)
# Enums
enum_pattern = r"\benum\s+(?:class\s+)?(\w+)(?:\s*:\s*\w+)?(?:\s*\{|;)"
for match in re.finditer(enum_pattern, content):
enum_name = match.group(1)
is_enum_class = "enum class" in match.group(0)
exports.append(
{
"name": enum_name,
"type": "enum_class" if is_enum_class else "enum",
"line": content[: match.start()].count("\n") + 1,
"namespace": namespace,
}
)
# Unions
union_pattern = r"\bunion\s+(\w+)(?:\s*\{|;)"
for match in re.finditer(union_pattern, content):
exports.append(
{
"name": match.group(1),
"type": "union",
"line": content[: match.start()].count("\n") + 1,
"namespace": namespace,
}
)
# Typedefs and using declarations
typedef_pattern = r"\btypedef\s+.*?\s+(\w+)\s*;"
for match in re.finditer(typedef_pattern, content):
exports.append(
{
"name": match.group(1),
"type": "typedef",
"line": content[: match.start()].count("\n") + 1,
"namespace": namespace,
}
)
using_pattern = r"\busing\s+(\w+)\s*="
for match in re.finditer(using_pattern, content):
exports.append(
{
"name": match.group(1),
"type": "using_alias",
"line": content[: match.start()].count("\n") + 1,
"namespace": namespace,
}
)
# Global variables (non-static)
if not is_header:
var_pattern = (
r"^(?!static)(?:extern\s+)?(?:const\s+)?(?:[\w\s\*&:<>]+)\s+(\w+)\s*(?:=|;)"
)
for match in re.finditer(var_pattern, content, re.MULTILINE):
var_name = match.group(1)
if var_name not in [
"if",
"for",
"while",
"return",
"class",
"struct",
"enum",
"typedef",
"using",
]:
exports.append(
{
"name": var_name,
"type": "variable",
"line": content[: match.start()].count("\n") + 1,
"namespace": namespace,
"is_const": "const" in match.group(0),
"is_extern": "extern" in match.group(0),
}
)
return exports
extract_structure¶
Extract code structure from C/C++ file.
Extracts: - Namespaces - Classes and structs with inheritance - Functions and methods - Templates - Macros and preprocessor directives - Global variables - Operator overloads
| PARAMETER | DESCRIPTION |
|---|---|
content | C/C++ source code TYPE: |
file_path | Path to the file being analyzed TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
CodeStructure | CodeStructure object with extracted elements |
Source code in tenets/core/analysis/implementations/cpp_analyzer.py
def extract_structure(self, content: str, file_path: Path) -> CodeStructure:
"""Extract code structure from C/C++ file.
Extracts:
- Namespaces
- Classes and structs with inheritance
- Functions and methods
- Templates
- Macros and preprocessor directives
- Global variables
- Operator overloads
Args:
content: C/C++ source code
file_path: Path to the file being analyzed
Returns:
CodeStructure object with extracted elements
"""
structure = CodeStructure()
# Determine if it's C or C++
is_cpp = self._is_cpp_file(file_path, content)
structure.language_variant = "C++" if is_cpp else "C"
# Extract namespaces (C++ only)
if is_cpp:
namespace_pattern = r"namespace\s+(\w+)\s*\{"
for match in re.finditer(namespace_pattern, content):
structure.namespaces.append(
{"name": match.group(1), "line": content[: match.start()].count("\n") + 1}
)
# Extract classes and structs
class_pattern = r"(?:template\s*<[^>]+>\s*)?(?:struct|(?<!enum\s)class)\s+(\w+)(?:\s*:\s*((?:public|private|protected)\s+[\w:]+(?:\s*,\s*(?:public|private|protected)\s+[\w:]+)*))?"
for match in re.finditer(class_pattern, content):
class_name = match.group(1)
inheritance = match.group(2)
# Parse inheritance
bases = []
if inheritance:
for base in inheritance.split(","):
base = base.strip()
# Remove access specifier
base = re.sub(r"^(public|private|protected)\s+", "", base)
bases.append(base)
# Find class body
class_start = match.end()
class_body = self._extract_class_body(content, class_start)
# Extract methods and members
methods = []
fields = []
if class_body:
methods = self._extract_class_methods(class_body)
fields = self._extract_class_fields(class_body)
inner = match.group(0)
kw = "struct" if "struct" in inner else "class"
kw_pos = match.start() + inner.find(kw)
class_info = ClassInfo(
name=class_name,
line=content[: match.start()].count("\n") + 1,
bases=bases,
methods=methods,
fields=fields,
is_struct="struct" in match.group(0),
is_template=self._is_template_class(content, kw_pos),
)
structure.classes.append(class_info)
# Extract standalone functions
func_pattern = r"(?:template\s*<[^>]+>\s*)?(?:(?:inline|static|extern|virtual|explicit|constexpr)\s+)*(?:[\w\s\*&:<>]+)\s+(\w+)\s*\([^)]*\)(?:\s*const)?(?:\s*noexcept)?(?:\s*\{|;)"
for match in re.finditer(func_pattern, content, re.MULTILINE):
func_name = match.group(1)
# Filter out keywords and methods
if func_name in [
"if",
"for",
"while",
"switch",
"return",
"delete",
"new",
"throw",
"catch",
]:
continue
# Check if it's inside a class (simple heuristic)
if self._is_inside_class(content, match.start()):
continue
func_info = FunctionInfo(
name=func_name,
line=content[: match.start()].count("\n") + 1,
is_static="static" in match.group(0),
is_inline="inline" in match.group(0),
is_constexpr="constexpr" in match.group(0),
is_template="template" in content[max(0, match.start() - 100) : match.start()],
is_exported="static" not in match.group(0),
)
structure.functions.append(func_info)
# Extract templates
template_pattern = r"template\s*<([^>]+)>\s*(?:class|struct|typename|function)\s+(\w+)"
for match in re.finditer(template_pattern, content):
structure.templates.append(
{
"name": match.group(2),
"parameters": match.group(1),
"line": content[: match.start()].count("\n") + 1,
}
)
# Extract macros
macro_pattern = r"^\s*#define\s+(\w+)(?:\([^)]*\))?"
for match in re.finditer(macro_pattern, content, re.MULTILINE):
macro_name = match.group(1)
is_function_macro = "(" in match.group(0)
structure.macros.append(
{
"name": macro_name,
"line": content[: match.start()].count("\n") + 1,
"is_function_macro": is_function_macro,
}
)
# Extract global variables
global_var_pattern = (
r"^(?:static\s+)?(?:const\s+)?(?:[\w\s\*&:<>]+)\s+(\w+)\s*(?:=\s*[^;]+)?\s*;"
)
for match in re.finditer(global_var_pattern, content, re.MULTILINE):
var_name = match.group(1)
# Filter out function declarations and keywords
if var_name in ["if", "for", "while", "return", "class", "struct", "enum", "typedef"]:
continue
if not self._is_inside_class(content, match.start()) and not self._is_inside_function(
content, match.start()
):
structure.variables.append(
{
"name": var_name,
"line": content[: match.start()].count("\n") + 1,
"type": "global",
"is_static": "static" in match.group(0),
"is_const": "const" in match.group(0),
}
)
# Extract unions
union_pattern = r"union\s+(\w+)\s*\{"
for match in re.finditer(union_pattern, content):
structure.unions.append(
{"name": match.group(1), "line": content[: match.start()].count("\n") + 1}
)
# Extract operator overloads
operator_pattern = r"operator\s*(?:[\+\-\*\/\%\^\&\|\~\!\=\<\>\[\]\(\)]|\+\+|\-\-|\<\<|\>\>|\=\=|\!\=|\<\=|\>\=|\&\&|\|\||\+\=|\-\=|\*\=|\/\=|\%\=|\^\=|\&\=|\|\=|\<\<\=|\>\>\=|,|->\*?|new|delete)(?:\s*\[\])?"
operator_count = len(re.findall(operator_pattern, content))
structure.operator_overloads = operator_count
# Detect STL usage (boolean for test compatibility)
stl_types_found = self._detect_stl_usage(content)
structure.uses_stl = bool(stl_types_found)
structure.stl_types = stl_types_found # Optionally keep the list for other uses
# Detect smart pointers
structure.smart_pointers = self._detect_smart_pointers(content)
# Count lambda expressions
lambda_pattern = r"\[[^\]]*\]\s*\([^)]*\)\s*(?:->[\w\s]+)?\s*\{"
structure.lambda_count = len(re.findall(lambda_pattern, content))
return structure
calculate_complexity¶
Calculate complexity metrics for C/C++ code.
Calculates: - Cyclomatic complexity - Cognitive complexity - Preprocessor complexity - Template complexity - Memory management complexity
| PARAMETER | DESCRIPTION |
|---|---|
content | C/C++ source code TYPE: |
file_path | Path to the file being analyzed TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
ComplexityMetrics | ComplexityMetrics object with calculated metrics |
Source code in tenets/core/analysis/implementations/cpp_analyzer.py
def calculate_complexity(self, content: str, file_path: Path) -> ComplexityMetrics:
"""Calculate complexity metrics for C/C++ code.
Calculates:
- Cyclomatic complexity
- Cognitive complexity
- Preprocessor complexity
- Template complexity
- Memory management complexity
Args:
content: C/C++ source code
file_path: Path to the file being analyzed
Returns:
ComplexityMetrics object with calculated metrics
"""
metrics = ComplexityMetrics()
# Calculate cyclomatic complexity
complexity = 1
decision_keywords = [
r"\bif\b",
r"\belse\s+if\b",
r"\belse\b",
r"\bfor\b",
r"\bwhile\b",
r"\bdo\b",
r"\bswitch\b",
r"\bcase\b",
r"\bcatch\b",
r"\b&&\b",
r"\|\|",
r"\?",
]
for keyword in decision_keywords:
complexity += len(re.findall(keyword, content))
metrics.cyclomatic = complexity
# Calculate cognitive complexity
cognitive = 0
nesting_level = 0
max_nesting = 0
lines = content.split("\n")
for line in lines:
# Skip comments and preprocessor directives
if (
line.strip().startswith("//")
or line.strip().startswith("/*")
or line.strip().startswith("#")
):
continue
# Track nesting
opening_braces = line.count("{")
closing_braces = line.count("}")
nesting_level += opening_braces - closing_braces
max_nesting = max(max_nesting, nesting_level)
# Control structures with nesting penalty
control_patterns = [
(r"\bif\b", 1),
(r"\bfor\b", 1),
(r"\bwhile\b", 1),
(r"\bswitch\b", 1),
(r"\btry\b", 1),
(r"\bcatch\b", 1),
]
for pattern, weight in control_patterns:
if re.search(pattern, line):
cognitive += weight * (1 + max(0, nesting_level - 1))
metrics.cognitive = cognitive
metrics.max_depth = max_nesting
# Count code elements
metrics.line_count = len(lines)
metrics.code_lines = self._count_code_lines(content)
metrics.comment_lines = self._count_comment_lines(content)
metrics.comment_ratio = (
metrics.comment_lines / metrics.line_count if metrics.line_count > 0 else 0
)
# Count functions
metrics.function_count = len(re.findall(r"[\w\s\*&:<>]+\s+\w+\s*\([^)]*\)\s*\{", content))
# Count classes and structs
metrics.class_count = len(re.findall(r"\b(?:class|struct)\s+\w+", content))
# Template metrics
metrics.template_count = len(re.findall(r"template\s*<", content))
metrics.template_specializations = len(re.findall(r"template\s*<>", content))
# Preprocessor metrics
metrics.macro_count = len(re.findall(r"^\s*#define\s+", content, re.MULTILINE))
metrics.ifdef_count = len(re.findall(r"^\s*#if(?:def|ndef)?\s+", content, re.MULTILINE))
metrics.include_count = len(re.findall(r"^\s*#include\s+", content, re.MULTILINE))
# Memory management metrics
metrics.new_count = len(re.findall(r"\bnew\s+", content))
# Count delete and delete[]
metrics.delete_count = len(re.findall(r"\bdelete\s*(?:\[\])?", content))
metrics.malloc_count = len(re.findall(r"\bmalloc\s*\(", content))
metrics.free_count = len(re.findall(r"\bfree\s*\(", content))
# Smart pointer usage (count both types and factory helpers)
metrics.unique_ptr_count = len(re.findall(r"\bunique_ptr\s*<", content)) + len(
re.findall(r"(?:\b[\w:]+::)?make_unique(?:\s*<[^>]+>)?\s*\(", content)
)
metrics.shared_ptr_count = len(re.findall(r"\bshared_ptr\s*<", content)) + len(
re.findall(r"(?:\b[\w:]+::)?make_shared(?:\s*<[^>]+>)?\s*\(", content)
)
metrics.weak_ptr_count = len(re.findall(r"\bweak_ptr\s*<", content))
# RAII indicators
metrics.uses_raii = (
metrics.unique_ptr_count > 0 or metrics.shared_ptr_count > 0 or "RAII" in content
)
# Calculate memory safety score
manual_memory = (
metrics.new_count + metrics.delete_count + metrics.malloc_count + metrics.free_count
)
smart_memory = metrics.unique_ptr_count + metrics.shared_ptr_count
if manual_memory + smart_memory > 0:
metrics.memory_safety_score = smart_memory / (manual_memory + smart_memory)
else:
metrics.memory_safety_score = 1.0
# Calculate maintainability index
if metrics.code_lines > 0:
# Adjusted for C++ complexity
template_factor = 1 - (metrics.template_count * 0.02)
memory_factor = metrics.memory_safety_score
mi = (
171
- 5.2 * math.log(max(1, complexity))
- 0.23 * complexity
- 16.2 * math.log(metrics.code_lines)
+ 10 * template_factor
+ 15 * memory_factor
)
metrics.maintainability_index = max(0, min(100, mi))
return metrics
CSharpAnalyzer¶
Bases: LanguageAnalyzer
C# code analyzer with Unity3D support.
Provides comprehensive analysis for C# files including: - Using directives and namespace analysis - Class, interface, struct, enum, and record extraction - Property and event analysis - Async/await and Task-based patterns - LINQ query detection - Attribute processing - Unity3D specific patterns (MonoBehaviour, Coroutines, etc.) - .NET Framework/Core detection - Nullable reference types (C# 8+) - Pattern matching (C# 7+)
Supports modern C# features and Unity3D development patterns.
Initialize the C# analyzer with logger.
Source code in tenets/core/analysis/implementations/csharp_analyzer.py
extract_imports¶
Extract using directives from C# code.
Handles: - using statements: using System.Collections.Generic; - using static: using static System.Math; - using aliases: using Project = PC.MyCompany.Project; - global using (C# 10+): global using System.Text; - Unity-specific usings
| PARAMETER | DESCRIPTION |
|---|---|
content | C# source code TYPE: |
file_path | Path to the file being analyzed TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
List[ImportInfo] | List of ImportInfo objects with import details |
Source code in tenets/core/analysis/implementations/csharp_analyzer.py
def extract_imports(self, content: str, file_path: Path) -> List[ImportInfo]:
"""Extract using directives from C# code.
Handles:
- using statements: using System.Collections.Generic;
- using static: using static System.Math;
- using aliases: using Project = PC.MyCompany.Project;
- global using (C# 10+): global using System.Text;
- Unity-specific usings
Args:
content: C# source code
file_path: Path to the file being analyzed
Returns:
List of ImportInfo objects with import details
"""
imports: List[ImportInfo] = []
lines = content.splitlines()
current_namespace: Optional[str] = None
seen_code = False # stop parsing usings after first non-using code element at top-level
# Pre-compile patterns (hot path in large files)
namespace_re = re.compile(r"^\s*namespace\s+([\w\.]+)")
alias_re = re.compile(r"^\s*(?:(global)\s+)?using\s+([\w\.]+)\s*=\s*([^;]+?)\s*;")
using_re = re.compile(r"^\s*(?:(global)\s+)?using\s+(?:(static)\s+)?([\w\.]+)\s*;")
decl_re = re.compile(
r"^\s*(?:public\s+)?(?:partial\s+)?(?:abstract\s+)?(?:sealed\s+)?(?:class|interface|struct|enum|delegate|record)\b"
)
for i, line in enumerate(lines, 1):
stripped = line.strip()
if not stripped:
continue
# Skip single-line comments
if stripped.startswith("//"):
continue
# Namespace (track for nested usings)
m = namespace_re.match(line)
if m:
current_namespace = m.group(1)
# Don't treat namespace declaration itself as code for stopping further usings
continue
# Stop scanning after first real code (class/interface/etc.) at top-level
if decl_re.match(line):
seen_code = True
if seen_code:
# Still allow usings inside namespace blocks (indented) – C# allows that
# Only break if this is a top-level code declaration and not inside a namespace context yet
if current_namespace is None:
break
# Using alias
m = alias_re.match(line)
if m:
is_global = m.group(1) == "global"
alias = m.group(2)
target = m.group(3).strip()
base_for_category = target.split("<", 1)[0].strip()
category = self._categorize_import(base_for_category)
is_unity = self._is_unity_import(base_for_category)
imports.append(
ImportInfo(
module=target,
alias=alias,
line=i,
type="global_using_alias" if is_global else "using_alias",
is_relative=False,
category=category,
is_unity=is_unity,
namespace_context=current_namespace,
)
)
continue
# Standard / static / global usings
m = using_re.match(line)
if m:
is_global = m.group(1) == "global"
is_static = m.group(2) == "static"
ns = m.group(3)
category = self._categorize_import(ns)
is_unity = self._is_unity_import(ns)
if is_global:
import_type = "global_using"
elif is_static:
import_type = "using_static"
else:
import_type = "using"
imports.append(
ImportInfo(
module=ns,
line=i,
type=import_type,
is_relative=False,
category=category,
is_unity=is_unity,
namespace_context=current_namespace,
)
)
continue
# .csproj dependency parsing
if file_path.suffix.lower() == ".csproj":
imports.extend(self._extract_csproj_dependencies(content))
return imports
extract_exports¶
Extract public members from C# code.
In C#, public members are accessible from other assemblies. This includes public classes, interfaces, structs, enums, delegates, etc.
| PARAMETER | DESCRIPTION |
|---|---|
content | C# source code TYPE: |
file_path | Path to the file being analyzed TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
List[Dict[str, Any]] | List of exported (public) symbols |
Source code in tenets/core/analysis/implementations/csharp_analyzer.py
def extract_exports(self, content: str, file_path: Path) -> List[Dict[str, Any]]:
"""Extract public members from C# code.
In C#, public members are accessible from other assemblies.
This includes public classes, interfaces, structs, enums, delegates, etc.
Args:
content: C# source code
file_path: Path to the file being analyzed
Returns:
List of exported (public) symbols
"""
exports = []
# Extract namespace
namespace_match = re.search(r"^\s*namespace\s+([\w\.]+)", content, re.MULTILINE)
namespace = namespace_match.group(1) if namespace_match else ""
# Public classes (including Unity MonoBehaviours)
class_pattern = r"^\s*(?:public\s+)?(?:partial\s+)?(?:abstract\s+)?(?:sealed\s+)?(?:static\s+)?class\s+(\w+)(?:\s*:\s*([\w\.,\s]+))?"
for match in re.finditer(class_pattern, content, re.MULTILINE):
class_name = match.group(1)
inheritance = match.group(2)
modifiers = []
if "abstract" in match.group(0):
modifiers.append("abstract")
if "sealed" in match.group(0):
modifiers.append("sealed")
if "static" in match.group(0):
modifiers.append("static")
if "partial" in match.group(0):
modifiers.append("partial")
# Check if it's a Unity component
is_unity_component = False
unity_base_class = None
if inheritance:
if "MonoBehaviour" in inheritance:
is_unity_component = True
unity_base_class = "MonoBehaviour"
elif "ScriptableObject" in inheritance:
is_unity_component = True
unity_base_class = "ScriptableObject"
elif "Editor" in inheritance:
is_unity_component = True
unity_base_class = "Editor"
exports.append(
{
"name": class_name,
"type": "class",
"line": content[: match.start()].count("\n") + 1,
"namespace": namespace,
"modifiers": modifiers,
"inheritance": inheritance,
"is_unity_component": is_unity_component,
"unity_base_class": unity_base_class,
}
)
# Public interfaces
interface_pattern = r"^\s*(?:public\s+)?(?:partial\s+)?interface\s+(\w+)(?:<[^>]+>)?(?:\s*:\s*([\w\.,\s]+))?"
for match in re.finditer(interface_pattern, content, re.MULTILINE):
exports.append(
{
"name": match.group(1),
"type": "interface",
"line": content[: match.start()].count("\n") + 1,
"namespace": namespace,
"extends": match.group(2),
}
)
# Public structs
struct_pattern = r"^\s*(?:public\s+)?(?:readonly\s+)?(?:ref\s+)?struct\s+(\w+)"
for match in re.finditer(struct_pattern, content, re.MULTILINE):
modifiers = []
if "readonly" in match.group(0):
modifiers.append("readonly")
if "ref" in match.group(0):
modifiers.append("ref")
exports.append(
{
"name": match.group(1),
"type": "struct",
"line": content[: match.start()].count("\n") + 1,
"namespace": namespace,
"modifiers": modifiers,
}
)
# Public enums (support both 'enum' and 'enum class' styles)
enum_pattern = r"^\s*(?:public\s+)?enum(?:\s+class)?\s+(\w+)(?:\s*:\s*([\w\.]+))?"
for match in re.finditer(enum_pattern, content, re.MULTILINE):
enum_type = "enum_class" if "enum class" in match.group(0) else "enum"
exports.append(
{
"name": match.group(1),
"type": enum_type,
"line": content[: match.start()].count("\n") + 1,
"namespace": namespace,
"base_type": match.group(2),
}
)
# Public delegates
delegate_pattern = r"^\s*(?:public\s+)?delegate\s+(\w+)\s+(\w+(?:<[^>]+>)?)\s*\([^)]*\)"
for match in re.finditer(delegate_pattern, content, re.MULTILINE):
exports.append(
{
"name": match.group(2),
"type": "delegate",
"return_type": match.group(1),
"line": content[: match.start()].count("\n") + 1,
"namespace": namespace,
}
)
# Public records (C# 9+)
record_pattern = r"^\s*(?:public\s+)?record\s+(?:class\s+|struct\s+)?(\w+)"
for match in re.finditer(record_pattern, content, re.MULTILINE):
record_type = "record_struct" if "struct" in match.group(0) else "record"
exports.append(
{
"name": match.group(1),
"type": record_type,
"line": content[: match.start()].count("\n") + 1,
"namespace": namespace,
}
)
return exports
extract_structure¶
Extract code structure from C# file.
Extracts: - Namespace declarations - Classes with inheritance and interfaces - Properties with getters/setters - Methods including async methods - Events and delegates - Unity-specific components (MonoBehaviours, Coroutines) - LINQ queries - Attributes
| PARAMETER | DESCRIPTION |
|---|---|
content | C# source code TYPE: |
file_path | Path to the file being analyzed TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
CodeStructure | CodeStructure object with extracted elements |
Source code in tenets/core/analysis/implementations/csharp_analyzer.py
def extract_structure(self, content: str, file_path: Path) -> CodeStructure:
"""Extract code structure from C# file.
Extracts:
- Namespace declarations
- Classes with inheritance and interfaces
- Properties with getters/setters
- Methods including async methods
- Events and delegates
- Unity-specific components (MonoBehaviours, Coroutines)
- LINQ queries
- Attributes
Args:
content: C# source code
file_path: Path to the file being analyzed
Returns:
CodeStructure object with extracted elements
"""
structure = CodeStructure()
# Extract namespace
namespace_match = re.search(r"^\s*namespace\s+([\w\.]+)", content, re.MULTILINE)
if namespace_match:
structure.namespace = namespace_match.group(1)
# Detect if it's a Unity script
structure.is_unity_script = self._is_unity_script(content)
# Extract classes
# Capture any stacked attribute blocks immediately preceding the class declaration in a named group
# so we don't rely on a fragile backward scan that fails when the regex itself already consumed them.
class_pattern = (
r"(?:^|\n)\s*(?P<attr_block>(?:\[[^\]]+\]\s*)*)"
r"(?:(?P<visibility>public|private|protected|internal)\s+)?"
r"(?:(?P<partial>partial)\s+)?(?:(?P<abstract>abstract)\s+)?(?:(?P<sealed>sealed)\s+)?(?:(?P<static>static)\s+)?"
r"class\s+(?P<class_name>\w+)(?:<(?P<generics>[^>]+)>)?(?:\s*:\s*(?P<inheritance>[\w\.,\s<>]+))?"
)
for match in re.finditer(class_pattern, content):
attr_block = match.group("attr_block") or ""
class_name = match.group("class_name") or ""
generics = match.group("generics")
inheritance = match.group("inheritance")
# Prefer directly captured attribute block; fallback to legacy backward scan only if empty
attributes = self._extract_attributes(attr_block) if attr_block else []
if not attributes:
# Legacy backward scan (kept for robustness in edge cases where regex miss might occur)
start_line_index = content[: match.start()].count("\n")
lines = content.splitlines()
attr_lines: List[str] = []
line_cursor = start_line_index - 1
while line_cursor >= 0:
line_text = lines[line_cursor].strip()
if not line_text or not line_text.startswith("["):
break
attr_lines.insert(0, line_text)
line_cursor -= 1
if attr_lines:
attributes = self._extract_attributes("\n".join(attr_lines))
# Collect modifiers
modifiers: List[str] = []
for key in ["partial", "abstract", "sealed", "static"]:
if match.group(key):
modifiers.append(match.group(key))
visibility = match.group("visibility") or None
# Parse inheritance
bases = []
interfaces = []
is_monobehaviour = False
is_scriptable_object = False
if inheritance:
for item in inheritance.split(","):
item = item.strip()
if item == "MonoBehaviour":
is_monobehaviour = True
bases.append(item)
elif item == "ScriptableObject":
is_scriptable_object = True
bases.append(item)
elif item.startswith("I"): # Convention for interfaces
interfaces.append(item)
else:
bases.append(item)
# Find class body
class_body = self._extract_class_body(content, match.end())
# Extract class components
methods = []
properties = []
fields = []
events = []
unity_methods = []
coroutines = []
if class_body:
methods = self._extract_methods(class_body)
properties = self._extract_properties(class_body)
fields = self._extract_fields(class_body)
events = self._extract_events(class_body)
if is_monobehaviour or is_scriptable_object:
unity_methods = self._extract_unity_methods(class_body)
coroutines = self._extract_coroutines(class_body)
class_info = ClassInfo(
name=class_name,
line=content[: match.start()].count("\n") + 1,
generics=generics,
bases=bases,
interfaces=interfaces,
visibility=visibility,
modifiers=modifiers,
methods=methods,
properties=properties,
fields=fields,
events=events,
attributes=attributes,
is_monobehaviour=is_monobehaviour,
is_scriptable_object=is_scriptable_object,
unity_methods=unity_methods,
coroutines=coroutines,
)
structure.classes.append(class_info)
# Extract interfaces
interface_pattern = r"(?:^|\n)\s*(?:public\s+)?(?:partial\s+)?interface\s+(\w+)(?:<([^>]+)>)?(?:\s*:\s*([\w\.,\s<>]+))?"
for match in re.finditer(interface_pattern, content):
interface_name = match.group(1)
generics = match.group(2)
extends = match.group(3)
# Extract interface methods
interface_body = self._extract_class_body(content, match.end())
methods = self._extract_interface_methods(interface_body) if interface_body else []
structure.interfaces.append(
{
"name": interface_name,
"line": content[: match.start()].count("\n") + 1,
"generics": generics,
"extends": self._parse_interface_list(extends) if extends else [],
"methods": methods,
}
)
# Extract structs
struct_pattern = (
r"(?:^|\n)\s*(?:public\s+)?(?:readonly\s+)?(?:ref\s+)?struct\s+(\w+)(?:<([^>]+)>)?"
)
for match in re.finditer(struct_pattern, content):
struct_name = match.group(1)
generics = match.group(2)
modifiers = []
if "readonly" in match.group(0):
modifiers.append("readonly")
if "ref" in match.group(0):
modifiers.append("ref")
structure.structs.append(
{
"name": struct_name,
"line": content[: match.start()].count("\n") + 1,
"generics": generics,
"modifiers": modifiers,
}
)
# Extract enums
enum_pattern = r"(?:^|\n)\s*(?:public\s+)?enum\s+(\w+)(?:\s*:\s*(\w+))?"
for match in re.finditer(enum_pattern, content):
enum_name = match.group(1)
base_type = match.group(2)
# Extract enum values
enum_body = self._extract_class_body(content, match.end())
values = self._extract_enum_values(enum_body) if enum_body else []
structure.enums.append(
{
"name": enum_name,
"line": content[: match.start()].count("\n") + 1,
"base_type": base_type,
"values": values,
}
)
# Extract delegates
delegate_pattern = r"(?:^|\n)\s*(?:public\s+)?delegate\s+(\w+)\s+(\w+)\s*\(([^)]*)\)"
for match in re.finditer(delegate_pattern, content):
structure.delegates.append(
{
"return_type": match.group(1),
"name": match.group(2),
"parameters": self._parse_parameters(match.group(3)),
"line": content[: match.start()].count("\n") + 1,
}
)
# Extract global functions (rare in C# but possible)
structure.functions = self._extract_global_functions(content)
# Extract LINQ queries
structure.linq_queries = self._extract_linq_queries(content)
# Count async methods
structure.async_method_count = len(re.findall(r"\basync\s+(?:Task|ValueTask)", content))
# Count lambda expressions
structure.lambda_count = len(re.findall(r"=>\s*(?:\{|[^;{]+;)", content))
# Detect framework
structure.framework = self._detect_framework(content)
# Check for test file
structure.is_test_file = (
"Test" in file_path.name
or file_path.name.endswith("Tests.cs")
or file_path.name.endswith("Test.cs")
or any(part in ["Tests", "Test"] for part in file_path.parts)
)
return structure
calculate_complexity¶
Calculate complexity metrics for C# code.
Calculates: - Cyclomatic complexity - Cognitive complexity - Unity-specific complexity (Coroutines, Update methods) - Async/await complexity - LINQ complexity - Exception handling complexity
| PARAMETER | DESCRIPTION |
|---|---|
content | C# source code TYPE: |
file_path | Path to the file being analyzed TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
ComplexityMetrics | ComplexityMetrics object with calculated metrics |
Source code in tenets/core/analysis/implementations/csharp_analyzer.py
def calculate_complexity(self, content: str, file_path: Path) -> ComplexityMetrics:
"""Calculate complexity metrics for C# code.
Calculates:
- Cyclomatic complexity
- Cognitive complexity
- Unity-specific complexity (Coroutines, Update methods)
- Async/await complexity
- LINQ complexity
- Exception handling complexity
Args:
content: C# source code
file_path: Path to the file being analyzed
Returns:
ComplexityMetrics object with calculated metrics
"""
metrics = ComplexityMetrics()
# Calculate cyclomatic complexity
complexity = 1
decision_keywords = [
r"\bif\b",
r"\belse\s+if\b",
r"\belse\b",
r"\bfor\b",
r"\bforeach\b",
r"\bwhile\b",
r"\bdo\b",
r"\bswitch\b",
r"\bcase\b",
r"\bcatch\b",
r"\b&&\b",
r"\|\|",
r"\?\s*[^:]+\s*:", # Ternary operator
r"\?\?", # Null coalescing operator
r"\?\.(?!\s*\[)", # Null conditional operator (not including ?.[])
]
for keyword in decision_keywords:
complexity += len(re.findall(keyword, content))
# Add complexity for pattern matching (C# 7+)
# "is" patterns
complexity += len(re.findall(r"\bis\s+\w+\s+\w+", content))
# Switch statements with when filters
complexity += len(re.findall(r"\bswitch\s*\(.*\)\s*\{[\s\S]*?\bwhen\b", content))
# Switch expressions with when clauses (=> and when)
complexity += len(re.findall(r"\bswitch\s*\{[\s\S]*?=>[\s\S]*?\bwhen\b", content))
metrics.cyclomatic = complexity
# Calculate cognitive complexity
cognitive = 0
nesting_level = 0
max_nesting = 0
lines = content.splitlines()
for line in lines:
# Skip comments
if line.strip().startswith("//"):
continue
# Track nesting
opening_braces = line.count("{")
closing_braces = line.count("}")
nesting_level += opening_braces - closing_braces
max_nesting = max(max_nesting, nesting_level)
# Control structures with nesting penalty
control_patterns = [
(r"\bif\b", 1),
(r"\belse\s+if\b", 1),
(r"\belse\b", 0),
(r"\bfor\b", 1),
(r"\bforeach\b", 1),
(r"\bwhile\b", 1),
(r"\bdo\b", 1),
(r"\bswitch\b", 1),
(r"\btry\b", 1),
(r"\bcatch\b", 1),
]
for pattern, weight in control_patterns:
if re.search(pattern, line):
cognitive += weight * (1 + max(0, nesting_level - 1))
metrics.cognitive = cognitive
metrics.max_depth = max_nesting
# Count code elements
metrics.line_count = len(lines)
metrics.code_lines = self._count_code_lines(content)
metrics.comment_lines = self._count_comment_lines(content)
metrics.comment_ratio = (
metrics.comment_lines / metrics.line_count if metrics.line_count > 0 else 0
)
# Count classes, interfaces, etc.
metrics.class_count = len(re.findall(r"\bclass\s+\w+", content))
metrics.interface_count = len(re.findall(r"\binterface\s+\w+", content))
metrics.struct_count = len(re.findall(r"\bstruct\s+\w+", content))
metrics.enum_count = len(re.findall(r"\benum\s+\w+", content))
# Count methods
metrics.method_count = len(
re.findall(
r"(?:public|private|protected|internal)\s+(?:static\s+)?(?:async\s+)?(?:override\s+)?(?:virtual\s+)?(?:[\w<>\[\]]+)\s+\w+\s*\([^)]*\)\s*\{",
content,
)
)
# Property metrics
metrics.property_count = len(
re.findall(
r"(?:public|private|protected|internal)\s+(?:static\s+)?(?:[\w<>\[\]]+)\s+\w+\s*\{\s*(?:get|set)",
content,
)
)
metrics.auto_property_count = len(re.findall(r"\{\s*get;\s*(?:set;)?\s*\}", content))
# Exception handling metrics
metrics.try_blocks = len(re.findall(r"\btry\s*\{", content))
metrics.catch_blocks = len(
re.findall(r"\bcatch(?:\s+when\s*\([^)]*\))?\s*(?:\([^)]*\))?\s*\{", content)
)
metrics.finally_blocks = len(re.findall(r"\bfinally\s*\{", content))
# Count both "throw;" and "throw new ..." forms
metrics.throw_statements = len(re.findall(r"\bthrow\b", content))
# Async/await metrics
metrics.async_methods = len(re.findall(r"\basync\s+(?:Task|ValueTask)", content))
metrics.await_statements = len(re.findall(r"\bawait\s+", content))
# LINQ metrics
metrics.linq_queries = len(re.findall(r"\bfrom\s+\w+\s+in\s+", content))
metrics.linq_methods = len(
re.findall(
r"\.\s*(?:Where|Select|OrderBy|GroupBy|Join|Any|All|First|Last|Single)\s*\(",
content,
)
)
# Unity-specific metrics
if self._is_unity_script(content):
metrics.unity_components = len(
re.findall(r":\s*(?:MonoBehaviour|ScriptableObject)", content)
)
metrics.coroutines = len(re.findall(r"\bIEnumerator\s+\w+\s*\(", content))
metrics.unity_methods = len(
re.findall(
r"\b(?:Start|Update|FixedUpdate|LateUpdate|OnEnable|OnDisable|Awake|OnDestroy|OnCollision(?:Enter|Exit|Stay)?|OnTrigger(?:Enter|Exit|Stay)?)\s*\(",
content,
)
)
metrics.serialize_fields = len(re.findall(r"\[SerializeField\]", content))
metrics.unity_events = len(re.findall(r"\bUnityEvent(?:<[^>]+>)?\s+\w+", content))
# Attribute metrics
metrics.attribute_count = len(re.findall(r"\[[A-Z]\w*(?:\([^)]*\))?\]", content))
# Nullable reference types (C# 8+): properties and locals/params with ? type, plus #nullable enable
nullable_types = len(re.findall(r"[\w<>\[\]]+\?\s+\w+\s*[;=,)\}]", content))
metrics.nullable_refs = nullable_types + len(re.findall(r"#nullable\s+enable", content))
# Calculate maintainability index
import math
if metrics.code_lines > 0:
# Adjusted for C#
async_factor = 1 - (metrics.async_methods * 0.01)
unity_factor = 1 - (getattr(metrics, "coroutines", 0) * 0.02)
mi = (
171
- 5.2 * math.log(max(1, complexity))
- 0.23 * complexity
- 16.2 * math.log(metrics.code_lines)
+ 10 * async_factor
+ 10 * unity_factor
)
metrics.maintainability_index = max(0, min(100, mi))
return metrics
CSSAnalyzer¶
Bases: LanguageAnalyzer
CSS code analyzer with preprocessor and framework support.
Provides comprehensive analysis for CSS files including: - CSS3 features and properties - SCSS/Sass preprocessor features - Less preprocessor features - PostCSS plugins and features - Tailwind CSS utility classes - UnoCSS atomic CSS - CSS-in-JS patterns - CSS Modules - BEM, OOCSS, SMACSS methodologies - Performance metrics - Browser compatibility - Accessibility considerations - Design system patterns
Supports modern CSS development practices and frameworks.
Initialize the CSS analyzer with logger.
Source code in tenets/core/analysis/implementations/css_analyzer.py
def __init__(self):
"""Initialize the CSS analyzer with logger."""
self.logger = get_logger(__name__)
# Tailwind utility patterns
self.tailwind_patterns = self._load_tailwind_patterns()
# UnoCSS patterns
self.unocss_patterns = self._load_unocss_patterns()
# CSS framework patterns
self.framework_patterns = self._load_framework_patterns()
extract_imports¶
Extract import statements from CSS.
Handles: - @import statements - @use (Sass) - @forward (Sass) - url() functions - CSS Modules composes
| PARAMETER | DESCRIPTION |
|---|---|
content | CSS source code TYPE: |
file_path | Path to the file being analyzed TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
List[ImportInfo] | List of ImportInfo objects with import details |
Source code in tenets/core/analysis/implementations/css_analyzer.py
def extract_imports(self, content: str, file_path: Path) -> List[ImportInfo]:
"""Extract import statements from CSS.
Handles:
- @import statements
- @use (Sass)
- @forward (Sass)
- url() functions
- CSS Modules composes
Args:
content: CSS source code
file_path: Path to the file being analyzed
Returns:
List of ImportInfo objects with import details
"""
imports = []
# Determine file type
ext = file_path.suffix.lower()
is_scss = ext in [".scss", ".sass"]
is_less = ext == ".less"
# @import statements
import_pattern = r'@import\s+(?:url\()?["\']([^"\']+)["\'](?:\))?(?:\s+([^;]+))?;'
for match in re.finditer(import_pattern, content):
import_path = match.group(1)
media_query = match.group(2)
imports.append(
ImportInfo(
module=import_path,
line=content[: match.start()].count("\n") + 1,
type="import",
is_relative=not import_path.startswith(("http://", "https://", "//")),
media_query=media_query.strip() if media_query else None,
category=self._categorize_css_import(import_path),
)
)
# @use statements (Sass)
if is_scss:
use_pattern = r'@use\s+["\']([^"\']+)["\'](?:\s+as\s+(\w+))?(?:\s+with\s*\(([^)]+)\))?;'
for match in re.finditer(use_pattern, content):
module_path = match.group(1)
namespace = match.group(2)
config = match.group(3)
imports.append(
ImportInfo(
module=module_path,
line=content[: match.start()].count("\n") + 1,
type="use",
is_relative=not module_path.startswith(("http://", "https://", "//")),
namespace=namespace,
config=config,
category=self._categorize_css_import(module_path),
)
)
# @forward statements (Sass)
forward_pattern = r'@forward\s+["\']([^"\']+)["\'](?:\s+(show|hide)\s+([^;]+))?;'
for match in re.finditer(forward_pattern, content):
module_path = match.group(1)
visibility_type = match.group(2)
visibility_items = match.group(3)
# Combine visibility type and items for easier testing
if visibility_type and visibility_items:
visibility = f"{visibility_type} {visibility_items.strip()}"
else:
visibility = None
imports.append(
ImportInfo(
module=module_path,
line=content[: match.start()].count("\n") + 1,
type="forward",
is_relative=not module_path.startswith(("http://", "https://", "//")),
visibility=visibility,
category=self._categorize_css_import(module_path),
)
)
# url() in properties (for fonts, images, etc.)
url_pattern = r'url\(["\']?([^"\')\s]+)["\']?\)'
for match in re.finditer(url_pattern, content):
url_path = match.group(1)
# Skip data URLs and already imported files
if url_path.startswith("data:") or any(imp.module == url_path for imp in imports):
continue
imports.append(
ImportInfo(
module=url_path,
line=content[: match.start()].count("\n") + 1,
type="url",
is_relative=not url_path.startswith(("http://", "https://", "//")),
category=self._categorize_url_import(url_path),
)
)
# CSS Modules composes
composes_pattern = r'composes:\s*([a-zA-Z0-9-_\s]+)\s+from\s+["\']([^"\']+)["\'];'
for match in re.finditer(composes_pattern, content):
classes = match.group(1)
module_path = match.group(2)
imports.append(
ImportInfo(
module=module_path,
line=content[: match.start()].count("\n") + 1,
type="composes",
is_relative=not module_path.startswith(("http://", "https://", "//")),
composes=classes.strip(),
# Alias for tests that expect composed_classes
visibility=None,
category="css_module",
)
)
# Backward compatibility: also attach composed_classes attribute dynamically
for imp in imports:
if imp.type == "composes" and getattr(imp, "composes", None):
# Some tests reference ImportInfo.composed_classes
try:
setattr(imp, "composed_classes", imp.composes)
except Exception:
pass
return imports
extract_exports¶
Extract exported elements from CSS.
In CSS context, exports are: - Classes that can be used by HTML - IDs - Custom properties (CSS variables) - Mixins (SCSS/Less) - Functions (SCSS) - Keyframe animations - Utility classes (Tailwind/UnoCSS)
| PARAMETER | DESCRIPTION |
|---|---|
content | CSS source code TYPE: |
file_path | Path to the file being analyzed TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
List[Dict[str, Any]] | List of exported elements |
Source code in tenets/core/analysis/implementations/css_analyzer.py
def extract_exports(self, content: str, file_path: Path) -> List[Dict[str, Any]]:
"""Extract exported elements from CSS.
In CSS context, exports are:
- Classes that can be used by HTML
- IDs
- Custom properties (CSS variables)
- Mixins (SCSS/Less)
- Functions (SCSS)
- Keyframe animations
- Utility classes (Tailwind/UnoCSS)
Args:
content: CSS source code
file_path: Path to the file being analyzed
Returns:
List of exported elements
"""
exports = []
# Parse CSS
ext = file_path.suffix.lower()
is_scss = ext in [".scss", ".sass"]
parser = CSSParser(content, is_scss)
parser.parse()
# Export CSS classes (from selectors only)
classes: Set[str] = set()
for rule in parser.rules:
selector = rule.get("selector", "")
for match in re.finditer(r"\.([a-zA-Z0-9_\\:-]+)", selector):
class_name = match.group(1)
if class_name not in classes:
classes.add(class_name)
pos = content.find("." + class_name)
exports.append(
{
"name": class_name,
"type": "class",
"line": (content[:pos].count("\n") + 1) if pos != -1 else None,
}
)
# Export IDs (from selectors only, avoid hex colors)
ids: Set[str] = set()
for rule in parser.rules:
selector = rule.get("selector", "")
for match in re.finditer(r"#([a-zA-Z0-9_-]+)", selector):
id_name = match.group(1)
if id_name not in ids:
ids.add(id_name)
pos = content.find("#" + id_name)
exports.append(
{
"name": id_name,
"type": "id",
"line": (content[:pos].count("\n") + 1) if pos != -1 else None,
}
)
# Export custom properties
for prop_name, prop_value in parser.custom_properties.items():
exports.append(
{
"name": prop_name,
"type": "custom_property",
"value": prop_value,
}
)
# Export SCSS variables, mixins, functions
if is_scss:
for var_name, var_value in parser.variables.items():
exports.append(
{
"name": var_name,
"type": "scss_variable",
"value": var_value,
}
)
for mixin in parser.mixins:
exports.append(
{
"name": mixin["name"],
"type": "mixin",
"params": mixin["params"],
}
)
for func in parser.functions:
exports.append(
{
"name": func["name"],
"type": "function",
"params": func["params"],
}
)
# Export keyframes
for keyframe in parser.keyframes:
exports.append(
{
"name": keyframe["name"],
"type": "keyframe",
}
)
# Export utility classes (Tailwind/UnoCSS)
if self._is_utility_css(content):
utility_classes = self._extract_utility_classes(content)
for util_class in utility_classes:
exports.append(
{
"name": util_class,
"type": "utility_class",
"framework": self._detect_utility_framework(content),
}
)
return exports
extract_structure¶
Extract CSS document structure.
Extracts: - Rules and selectors - Media queries - CSS architecture patterns - Framework usage - Design tokens - Component structure
| PARAMETER | DESCRIPTION |
|---|---|
content | CSS source code TYPE: |
file_path | Path to the file being analyzed TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
CodeStructure | CodeStructure object with extracted elements |
Source code in tenets/core/analysis/implementations/css_analyzer.py
def extract_structure(self, content: str, file_path: Path) -> CodeStructure:
"""Extract CSS document structure.
Extracts:
- Rules and selectors
- Media queries
- CSS architecture patterns
- Framework usage
- Design tokens
- Component structure
Args:
content: CSS source code
file_path: Path to the file being analyzed
Returns:
CodeStructure object with extracted elements
"""
structure = CodeStructure()
# Parse CSS
ext = file_path.suffix.lower()
is_scss = ext in [".scss", ".sass"]
is_less = ext == ".less"
parser = CSSParser(content, is_scss)
parser.parse()
# Store parsed data
structure.rules = parser.rules
structure.variables = parser.variables
structure.custom_properties = parser.custom_properties
structure.mixins = parser.mixins
structure.functions = parser.functions
structure.keyframes = parser.keyframes
structure.media_queries = parser.media_queries
structure.supports_rules = parser.supports_rules
structure.max_nesting = parser.max_nesting
# Detect CSS methodology
structure.uses_bem = self._detect_bem(content)
structure.uses_oocss = self._detect_oocss(content)
structure.uses_smacss = self._detect_smacss(content)
structure.uses_atomic = self._detect_atomic_css(content)
# Detect frameworks
structure.is_tailwind = self._detect_tailwind(content, file_path)
structure.is_unocss = self._detect_unocss(content, file_path)
structure.is_bootstrap = self._detect_bootstrap(content)
structure.is_bulma = self._detect_bulma(content)
structure.is_material = self._detect_material(content)
# Count selectors by type (from selectors only)
selectors_joined = ",".join(rule.get("selector", "") for rule in parser.rules)
structure.element_selectors = len(
re.findall(r"(?:(?<=^)|(?<=[\s>+~,(]))[a-zA-Z][a-zA-Z0-9-]*", selectors_joined)
)
structure.class_selectors = len(re.findall(r"\.[a-zA-Z0-9_\\:-]+", selectors_joined))
structure.id_selectors = len(re.findall(r"#[a-zA-Z0-9_-]+", selectors_joined))
structure.attribute_selectors = len(re.findall(r"\[[^\]]+\]", selectors_joined))
structure.pseudo_classes = len(re.findall(r":(?!:)[a-z-]+(?:\([^)]*\))?", selectors_joined))
structure.pseudo_elements = len(re.findall(r"::[a-z-]+", selectors_joined))
# Count CSS3 features
structure.flexbox_usage = len(re.findall(r"display\s*:\s*(?:inline-)?flex", content))
structure.grid_usage = len(re.findall(r"display\s*:\s*grid", content))
structure.custom_property_usage = len(re.findall(r"var\(--[^)]+\)", content))
structure.calc_usage = len(re.findall(r"calc\([^)]+\)", content))
structure.transform_usage = len(re.findall(r"transform\s*:", content))
structure.transition_usage = len(re.findall(r"transition\s*:", content))
structure.animation_usage = len(re.findall(r"animation\s*:", content))
# Count responsive features
structure.media_query_count = len(parser.media_queries)
structure.viewport_units = len(re.findall(r"\d+(?:vw|vh|vmin|vmax)\b", content))
structure.container_queries = len(re.findall(r"@container\s+", content))
# Count modern CSS features
structure.css_nesting = len(
re.findall(r"&\s*[{:.]", content)
) + self._count_nested_selectors(content)
structure.has_layers = bool(re.search(r"@layer\s+", content))
structure.has_cascade_layers = len(re.findall(r"@layer\s+[a-z-]+\s*[{,]", content))
# Design system detection
structure.has_design_tokens = self._detect_design_tokens(content)
structure.color_variables = self._count_color_variables(parser.custom_properties)
structure.spacing_variables = self._count_spacing_variables(parser.custom_properties)
structure.typography_variables = self._count_typography_variables(parser.custom_properties)
# Component-based structure
structure.component_count = self._count_components(content)
structure.utility_count = self._count_utilities(content)
# PostCSS features
structure.uses_postcss = self._detect_postcss(content, file_path)
structure.postcss_plugins = self._detect_postcss_plugins(content)
# CSS-in-JS patterns
structure.is_css_modules = self._detect_css_modules(content, file_path)
structure.is_styled_components = self._detect_styled_components(content)
# Performance indicators
structure.unused_variables = self._find_unused_variables(content, parser)
structure.duplicate_properties = self._find_duplicate_properties(parser.rules)
structure.vendor_prefixes = len(re.findall(r"-(?:webkit|moz|ms|o)-", content))
# Accessibility
structure.focus_styles = len(re.findall(r":focus\s*[{,]", content))
structure.focus_visible = len(re.findall(r":focus-visible\s*[{,]", content))
structure.reduced_motion = len(re.findall(r"prefers-reduced-motion", content))
structure.high_contrast = len(re.findall(r"prefers-contrast", content))
structure.color_scheme = len(re.findall(r"prefers-color-scheme", content))
return structure
calculate_complexity¶
Calculate complexity metrics for CSS.
Calculates: - Selector complexity - Specificity metrics - Rule complexity - Nesting depth - Framework complexity - Performance score - Maintainability index
| PARAMETER | DESCRIPTION |
|---|---|
content | CSS source code TYPE: |
file_path | Path to the file being analyzed TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
ComplexityMetrics | ComplexityMetrics object with calculated metrics |
Source code in tenets/core/analysis/implementations/css_analyzer.py
def calculate_complexity(self, content: str, file_path: Path) -> ComplexityMetrics:
"""Calculate complexity metrics for CSS.
Calculates:
- Selector complexity
- Specificity metrics
- Rule complexity
- Nesting depth
- Framework complexity
- Performance score
- Maintainability index
Args:
content: CSS source code
file_path: Path to the file being analyzed
Returns:
ComplexityMetrics object with calculated metrics
"""
metrics = ComplexityMetrics()
# Parse CSS
ext = file_path.suffix.lower()
is_scss = ext in [".scss", ".sass"]
parser = CSSParser(content, is_scss)
parser.parse()
# Basic metrics
lines = content.split("\n")
metrics.line_count = len(lines)
metrics.code_lines = len([l for l in lines if l.strip() and not l.strip().startswith("//")])
metrics.comment_lines = len(re.findall(r"/\*.*?\*/", content, re.DOTALL))
if is_scss:
metrics.comment_lines += len([l for l in lines if l.strip().startswith("//")])
# Rule metrics
metrics.total_rules = len(parser.rules)
metrics.total_selectors = sum(len(rule["selector"].split(",")) for rule in parser.rules)
# Calculate average specificity
total_specificity = [0, 0, 0]
max_specificity = [0, 0, 0]
for rule in parser.rules:
spec = rule["specificity"]
total_specificity[0] += spec[0]
total_specificity[1] += spec[1]
total_specificity[2] += spec[2]
if spec[0] > max_specificity[0]:
max_specificity = spec
elif spec[0] == max_specificity[0] and spec[1] > max_specificity[1]:
max_specificity = spec
elif (
spec[0] == max_specificity[0]
and spec[1] == max_specificity[1]
and spec[2] > max_specificity[2]
):
max_specificity = spec
if metrics.total_rules > 0:
metrics.avg_specificity = [
total_specificity[0] / metrics.total_rules,
total_specificity[1] / metrics.total_rules,
total_specificity[2] / metrics.total_rules,
]
else:
metrics.avg_specificity = [0, 0, 0]
metrics.max_specificity = max_specificity
# Selector complexity
metrics.complex_selectors = 0
metrics.overqualified_selectors = 0
for rule in parser.rules:
selector = rule["selector"]
# Complex selector (too many parts)
if len(selector.split()) > 3:
metrics.complex_selectors += 1
# Overqualified (element with class/id)
if re.search(r"[a-z]+\.[a-z-]+|[a-z]+#[a-z-]+", selector, re.IGNORECASE):
metrics.overqualified_selectors += 1
# Important usage
metrics.important_count = len(re.findall(r"!important", content))
# Media query complexity
metrics.media_query_count = len(parser.media_queries)
metrics.media_query_complexity = sum(
len(mq["condition"].split("and")) for mq in parser.media_queries
)
# Nesting depth (for SCSS)
metrics.max_nesting_depth = parser.max_nesting
# Color usage
metrics.unique_colors = len(
set(
re.findall(
r"#[0-9a-fA-F]{3,8}|rgb\([^)]+\)|rgba\([^)]+\)|hsl\([^)]+\)|hsla\([^)]+\)",
content,
)
)
)
# Font usage
metrics.unique_fonts = len(set(re.findall(r"font-family\s*:\s*([^;]+);", content)))
# Z-index usage
z_indices = re.findall(r"z-index\s*:\s*(-?\d+)", content)
metrics.z_index_count = len(z_indices)
if z_indices:
metrics.max_z_index = max(int(z) for z in z_indices)
else:
metrics.max_z_index = 0
# File size metrics
metrics.file_size = len(content.encode("utf-8"))
metrics.gzip_ratio = self._estimate_gzip_ratio(content)
# Framework-specific metrics
if self._detect_tailwind(content, file_path):
metrics.tailwind_classes = self._count_tailwind_classes(content)
metrics.custom_utilities = self._count_custom_utilities(content)
if self._detect_unocss(content, file_path):
metrics.unocss_classes = self._count_unocss_classes(content)
# Calculate CSS complexity score
complexity_score = (
metrics.total_rules * 0.1
+ metrics.complex_selectors * 2
+ metrics.overqualified_selectors * 1.5
+ metrics.important_count * 3
+ metrics.max_nesting_depth * 1
+ (metrics.max_specificity[0] * 10) # IDs weighted heavily
+ (metrics.max_specificity[1] * 2) # Classes
+ (metrics.max_specificity[2] * 0.5) # Elements
)
metrics.complexity_score = complexity_score
# Performance score
performance_score = 100
# Deduct for complexity
performance_score -= min(30, complexity_score / 10)
# Deduct for !important
performance_score -= min(20, metrics.important_count * 2)
# Deduct for deep nesting
performance_score -= min(10, metrics.max_nesting_depth * 2)
# Deduct for excessive specificity
performance_score -= min(10, metrics.max_specificity[0] * 5)
# Bonus for CSS variables usage
if len(parser.custom_properties) > 0:
performance_score += min(10, len(parser.custom_properties) * 0.5)
metrics.performance_score = max(0, performance_score)
# Calculate maintainability index
import math
if metrics.code_lines > 0:
# Factors affecting CSS maintainability
specificity_factor = 1 - (sum(metrics.avg_specificity) * 0.1)
important_factor = 1 - (metrics.important_count * 0.02)
nesting_factor = 1 - (metrics.max_nesting_depth * 0.05)
organization_factor = 1 if len(parser.custom_properties) > 0 else 0.8
mi = (
171
- 5.2 * math.log(max(1, metrics.total_rules))
- 0.23 * complexity_score
- 16.2 * math.log(max(1, metrics.code_lines))
+ 20 * specificity_factor
+ 10 * important_factor
+ 10 * nesting_factor
+ 10 * organization_factor
)
metrics.maintainability_index = max(0, min(100, mi))
else:
metrics.maintainability_index = 100
return metrics
DartAnalyzer¶
Bases: LanguageAnalyzer
Dart code analyzer with Flutter support.
Provides comprehensive analysis for Dart files including: - Import and export directives - Part and library declarations - Classes with mixins and extensions - Null safety features (?, !, late) - Async/await, Future, and Stream handling - Flutter widgets and lifecycle methods - Factory and named constructors - Extension methods - Annotations and metadata - Generics and type parameters
Supports Dart 2.x with null safety and Flutter framework patterns.
Initialize the Dart analyzer with logger.
Source code in tenets/core/analysis/implementations/dart_analyzer.py
extract_imports¶
Extract import, export, part, and library directives from Dart code.
Handles: - import statements: import 'package:flutter/material.dart'; - export statements: export 'src/widget.dart'; - part statements: part 'implementation.dart'; - part of statements: part of 'library.dart'; - library declarations: library my_library; - Conditional imports: import 'stub.dart' if (dart.library.io) 'io.dart'; - Show/hide clauses: import 'dart:math' show Random hide PI; - Deferred imports: import 'big_lib.dart' deferred as big;
| PARAMETER | DESCRIPTION |
|---|---|
content | Dart source code TYPE: |
file_path | Path to the file being analyzed TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
List[ImportInfo] | List of ImportInfo objects with import details |
Source code in tenets/core/analysis/implementations/dart_analyzer.py
def extract_imports(self, content: str, file_path: Path) -> List[ImportInfo]:
"""Extract import, export, part, and library directives from Dart code.
Handles:
- import statements: import 'package:flutter/material.dart';
- export statements: export 'src/widget.dart';
- part statements: part 'implementation.dart';
- part of statements: part of 'library.dart';
- library declarations: library my_library;
- Conditional imports: import 'stub.dart' if (dart.library.io) 'io.dart';
- Show/hide clauses: import 'dart:math' show Random hide PI;
- Deferred imports: import 'big_lib.dart' deferred as big;
Args:
content: Dart source code
file_path: Path to the file being analyzed
Returns:
List of ImportInfo objects with import details
"""
imports = []
lines = content.split("\n")
for i, line in enumerate(lines, 1):
# Skip comments
if line.strip().startswith("//"):
continue
# Import statements - handle more complex patterns with show/hide
# First, try to extract the basic import and parse show/hide separately
basic_import_pattern = r"^\s*import\s+['\"]([^'\"]+)['\"](?:\s+if\s*\([^)]+\)\s*['\"][^'\"]+['\"]*)?(?:\s+deferred)?(?:\s+as\s+(\w+))?(.*?);"
match = re.match(basic_import_pattern, line)
if match:
module_path = match.group(1)
alias = match.group(2)
show_hide_part = match.group(3) if match.group(3) else ""
# Parse show/hide clauses
show_symbols = []
hide_symbols = []
is_deferred = "deferred" in line
# Extract show clause
show_match = re.search(r"\bshow\s+([^;]+?)(?:\s+hide|$)", show_hide_part + " ")
if show_match:
show_symbols = self._parse_symbols(show_match.group(1))
# Extract hide clause
hide_match = re.search(r"\bhide\s+([^;]+?)(?:\s+show|$)", show_hide_part + " ")
if hide_match:
hide_symbols = self._parse_symbols(hide_match.group(1))
# Determine import type
import_type = "import"
is_package = module_path.startswith("package:")
is_dart_core = module_path.startswith("dart:")
is_relative = module_path.startswith("../") or module_path.startswith("./")
# Categorize the import
category = self._categorize_import(module_path)
imports.append(
ImportInfo(
module=module_path,
alias=alias,
line=i,
type=import_type,
is_relative=is_relative,
is_package=is_package,
is_dart_core=is_dart_core,
is_deferred=is_deferred,
category=category,
show_symbols=show_symbols if show_symbols else [],
hide_symbols=hide_symbols if hide_symbols else [],
)
)
# Export statements
export_pattern = r"""
^\s*export\s+
['"]([^'"]+)['"]\s*
(?:show\s+([^;]+))?\s*
(?:hide\s+([^;]+))?\s*
;
"""
match = re.match(export_pattern, line, re.VERBOSE)
if match:
module_path = match.group(1)
show_clause = match.group(2)
hide_clause = match.group(3)
imports.append(
ImportInfo(
module=module_path,
line=i,
type="export",
is_relative=not module_path.startswith("package:")
and not module_path.startswith("dart:"),
show_symbols=self._parse_symbols(show_clause) if show_clause else [],
hide_symbols=self._parse_symbols(hide_clause) if hide_clause else [],
category=self._categorize_import(module_path),
)
)
# Part statements
part_pattern = r"^\s*part\s+['\"]([^'\"]+)['\"]\s*;"
match = re.match(part_pattern, line)
if match:
imports.append(
ImportInfo(
module=match.group(1),
line=i,
type="part",
is_relative=True,
is_part_file=True,
)
)
# Part of statements
part_of_pattern = r"^\s*part\s+of\s+['\"]?([^'\";\s]+)['\"]?\s*;"
match = re.match(part_of_pattern, line)
if match:
imports.append(
ImportInfo(
module=match.group(1),
line=i,
type="part_of",
is_relative=False,
is_library_part=True,
)
)
# Library declaration
library_pattern = r"^\s*library\s+(\w+(?:\.\w+)*)\s*;"
match = re.match(library_pattern, line)
if match:
imports.append(
ImportInfo(
module=match.group(1),
line=i,
type="library",
is_relative=False,
is_library_declaration=True,
)
)
# Handle conditional, multi-line imports like:
# import 'stub.dart'
# if (dart.library.io) 'io_implementation.dart'
# if (dart.library.html) 'web_implementation.dart';
cond_import_pattern = (
r"import\s+['\"]([^'\"]+)['\"]\s*(?:\s*if\s*\([^)]+\)\s*['\"][^'\"]+['\"]\s*)+;"
)
for m in re.finditer(cond_import_pattern, content, re.MULTILINE):
first_module = m.group(1)
# Avoid duplicates if already added (e.g., if written in one line)
if not any(imp.module == first_module and imp.type == "import" for imp in imports):
imports.append(
ImportInfo(
module=first_module,
line=content[: m.start()].count("\n") + 1,
type="import",
is_relative=first_module.startswith("../") or first_module.startswith("./"),
is_package=first_module.startswith("package:"),
is_dart_core=first_module.startswith("dart:"),
category=self._categorize_import(first_module),
conditional=True,
)
)
return imports
extract_exports¶
Extract exported symbols from Dart code.
In Dart, exports include: - Public classes (not prefixed with _) - Public functions - Public variables and constants - Public typedefs - Public enums - Extension methods
| PARAMETER | DESCRIPTION |
|---|---|
content | Dart source code TYPE: |
file_path | Path to the file being analyzed TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
List[Dict[str, Any]] | List of exported symbols |
Source code in tenets/core/analysis/implementations/dart_analyzer.py
def extract_exports(self, content: str, file_path: Path) -> List[Dict[str, Any]]:
"""Extract exported symbols from Dart code.
In Dart, exports include:
- Public classes (not prefixed with _)
- Public functions
- Public variables and constants
- Public typedefs
- Public enums
- Extension methods
Args:
content: Dart source code
file_path: Path to the file being analyzed
Returns:
List of exported symbols
"""
exports = []
# Public classes (including abstract and mixins)
class_pattern = r"^\s*(?:abstract\s+)?(?:final\s+)?(?:base\s+)?(?:interface\s+)?(?:mixin\s+)?class\s+([A-Z]\w*)"
for match in re.finditer(class_pattern, content, re.MULTILINE):
class_name = match.group(1)
modifiers = []
match_str = match.group(0)
if "abstract" in match_str:
modifiers.append("abstract")
if "final" in match_str:
modifiers.append("final")
if "base" in match_str:
modifiers.append("base")
if "interface" in match_str:
modifiers.append("interface")
if "mixin" in match_str:
modifiers.append("mixin")
exports.append(
{
"name": class_name,
"type": "class",
"line": content[: match.start()].count("\n") + 1,
"modifiers": modifiers,
"is_public": True,
}
)
# Mixins
mixin_pattern = r"^\s*(?:base\s+)?mixin\s+([A-Z]\w*)"
for match in re.finditer(mixin_pattern, content, re.MULTILINE):
if not any(e["name"] == match.group(1) for e in exports): # Avoid duplicates
exports.append(
{
"name": match.group(1),
"type": "mixin",
"line": content[: match.start()].count("\n") + 1,
"is_public": True,
}
)
# Public functions (not starting with _), including async*, sync*
func_pattern = r"^\s*(?:Future<?[^>]*>?\s+|Stream<?[^>]*>?\s+|void\s+|[\w<>]+\s+)?([a-z]\w*)\s*(?:<[^>]+>)?\s*\([^\{]*\)\s*(?:(?:async|sync)\s*\*|async)?\s*(?:=>|\{)"
for match in re.finditer(func_pattern, content, re.MULTILINE):
func_name = match.group(1)
if not func_name.startswith("_"):
snippet = match.group(0)
exports.append(
{
"name": func_name,
"type": "function",
"line": content[: match.start()].count("\n") + 1,
"is_public": True,
"is_async": ("async" in snippet),
}
)
# Public variables and constants
var_pattern = r"^\s*(?:final\s+|const\s+|late\s+)?(?:static\s+)?(?:final\s+|const\s+)?(?:[\w<>?]+\s+)?([a-z]\w*)\s*(?:=|;)"
for match in re.finditer(var_pattern, content, re.MULTILINE):
var_name = match.group(1)
if not var_name.startswith("_") and var_name not in [
"if",
"for",
"while",
"return",
"class",
"import",
]:
var_type = "constant" if "const" in match.group(0) else "variable"
exports.append(
{
"name": var_name,
"type": var_type,
"line": content[: match.start()].count("\n") + 1,
"is_public": True,
"is_final": "final" in match.group(0),
"is_late": "late" in match.group(0),
}
)
# Enums
enum_pattern = r"^\s*enum\s+([A-Z]\w*)"
for match in re.finditer(enum_pattern, content, re.MULTILINE):
exports.append(
{
"name": match.group(1),
"type": "enum",
"line": content[: match.start()].count("\n") + 1,
"is_public": True,
}
)
# Typedefs
typedef_pattern = r"^\s*typedef\s+([A-Z]\w*)"
for match in re.finditer(typedef_pattern, content, re.MULTILINE):
exports.append(
{
"name": match.group(1),
"type": "typedef",
"line": content[: match.start()].count("\n") + 1,
"is_public": True,
}
)
# Extension methods
extension_pattern = r"^\s*extension\s+(?:([A-Z]\w*)\s+)?on\s+([A-Z]\w*)"
for match in re.finditer(extension_pattern, content, re.MULTILINE):
extension_name = match.group(1) or f"Extension on {match.group(2)}"
exports.append(
{
"name": extension_name,
"type": "extension",
"line": content[: match.start()].count("\n") + 1,
"on_type": match.group(2),
"is_public": True,
}
)
return exports
extract_structure¶
Extract code structure from Dart file.
Extracts: - Classes with inheritance, mixins, and interfaces - Constructors (default, named, factory) - Methods and getters/setters - Flutter widgets and lifecycle methods - Async functions and streams - Extension methods - Null safety features - Annotations
| PARAMETER | DESCRIPTION |
|---|---|
content | Dart source code TYPE: |
file_path | Path to the file being analyzed TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
CodeStructure | CodeStructure object with extracted elements |
Source code in tenets/core/analysis/implementations/dart_analyzer.py
def extract_structure(self, content: str, file_path: Path) -> CodeStructure:
"""Extract code structure from Dart file.
Extracts:
- Classes with inheritance, mixins, and interfaces
- Constructors (default, named, factory)
- Methods and getters/setters
- Flutter widgets and lifecycle methods
- Async functions and streams
- Extension methods
- Null safety features
- Annotations
Args:
content: Dart source code
file_path: Path to the file being analyzed
Returns:
CodeStructure object with extracted elements
"""
structure = CodeStructure()
# Detect if it's a Flutter file
structure.is_flutter = self._is_flutter_file(content)
# Extract classes
class_pattern = r"""
^\s*(?:@\w+(?:\([^)]*\))?\s*)* # Annotations
(?:(abstract)\s+)?
(?:(final)\s+)?
(?:(base)\s+)?
(?:(interface)\s+)?
(?:(mixin)\s+)?
(?:(sealed)\s+)?
class\s+(\w+)
(?:<([^>\n{}]*?)>+)? # Generics (tolerant of nested '>')
(?:\s+extends\s+([^\{]+?))?
(?:\s+with\s+([^\{]+?))?
(?:\s+implements\s+([^\{]+?))?
\s*\{
"""
for match in re.finditer(class_pattern, content, re.VERBOSE | re.MULTILINE):
class_name = match.group(7)
# Extract class modifiers
modifiers = []
if match.group(1):
modifiers.append("abstract")
if match.group(2):
modifiers.append("final")
if match.group(3):
modifiers.append("base")
if match.group(4):
modifiers.append("interface")
if match.group(5):
modifiers.append("mixin")
if match.group(6):
modifiers.append("sealed")
# Parse inheritance
extends = match.group(9).strip() if match.group(9) else None
mixins = self._parse_type_list(match.group(10)) if match.group(10) else []
implements = self._parse_type_list(match.group(11)) if match.group(11) else []
# Check if it's a Flutter widget
is_widget = False
widget_type = None
if extends:
# Prefer concrete State<T> first to avoid misclassification
if re.search(r"\bState<", extends):
is_widget = True
widget_type = "state"
elif "StatelessWidget" in extends:
is_widget = True
widget_type = "stateless"
elif "StatefulWidget" in extends:
is_widget = True
widget_type = "stateful"
elif "InheritedWidget" in extends:
is_widget = True
widget_type = "inherited"
# Extract class body
class_body = self._extract_class_body(content, match.end())
if class_body:
# Extract constructors
constructors = self._extract_constructors(class_body, class_name)
# Extract methods
methods = self._extract_methods(class_body)
# Extract fields
fields = self._extract_fields(class_body)
# Extract getters/setters
properties = self._extract_properties(class_body)
else:
constructors = []
methods = []
fields = []
properties = []
class_info = ClassInfo(
name=class_name,
line=content[: match.start()].count("\n") + 1,
modifiers=modifiers,
generics=match.group(8),
bases=[extends] if extends else [],
mixins=mixins,
interfaces=implements,
constructors=constructors,
methods=methods,
fields=fields,
properties=properties,
is_widget=is_widget,
widget_type=widget_type,
is_sealed="sealed" in modifiers,
)
# Balance generics angle brackets if regex captured incomplete nested generics
if class_info.generics:
try:
opens = class_info.generics.count("<")
closes = class_info.generics.count(">")
if opens > closes:
class_info.generics = class_info.generics + (">" * (opens - closes))
except Exception:
pass
structure.classes.append(class_info)
# Fallback: capture classes with complex generic bounds that the primary regex may miss
try:
existing = {c.name for c in structure.classes}
complex_class_pattern = r"""^\s*
(?:(abstract|final|base|interface|mixin|sealed)\s+)*
class\s+(\w+)\s*<([^\n{]+)>\s*
(?:extends\s+([^\n{]+?))?\s*
(?:with\s+([^\n{]+?))?\s*
(?:implements\s+([^\n{]+?))?\s*\{
"""
for m in re.finditer(complex_class_pattern, content, re.MULTILINE | re.VERBOSE):
name = m.group(2)
if name in existing:
continue
modifiers_raw = m.group(1) or ""
modifiers = [mod for mod in modifiers_raw.split() if mod]
generics = m.group(3).strip()
extends = m.group(4).strip() if m.group(4) else None
mixins = self._parse_type_list(m.group(5)) if m.group(5) else []
implements = self._parse_type_list(m.group(6)) if m.group(6) else []
structure.classes.append(
ClassInfo(
name=name,
line=content[: m.start()].count("\n") + 1,
generics=generics,
bases=[extends] if extends else [],
mixins=mixins,
interfaces=implements,
constructors=[],
methods=[],
fields=[],
properties=[],
modifiers=modifiers,
)
)
except Exception:
pass
# Extract mixins (standalone)
mixin_pattern = r"^\s*(?:base\s+)?mixin\s+(\w+)(?:<([^>]+)>)?(?:\s+on\s+([^{]+))?\s*\{"
for match in re.finditer(mixin_pattern, content, re.MULTILINE):
mixin_name = match.group(1)
# Avoid duplicates with mixin classes
if not any(c.name == mixin_name for c in structure.classes):
structure.mixins.append(
{
"name": mixin_name,
"line": content[: match.start()].count("\n") + 1,
"generics": match.group(2),
"on_types": self._parse_type_list(match.group(3)) if match.group(3) else [],
}
)
# Extract top-level functions
func_pattern = r"""
^\s*(?:@\w+(?:\([^)]*\))?\s*)* # Annotations
(?:(Future|Stream)(?:<[^>]+>)?\s+)?
(?:(void|[\w<>?]+|\([^)]+\))\s+)? # Return type or record type
([a-zA-Z_]\w*)\s*
(?:<[^>]+>)?\s* # Generic parameters
\(([^)]*)\)\s*
(?:(?:async|sync)\s*\*|async)?\s* # async, async*, or sync*
(?:=>|\{)
"""
for match in re.finditer(func_pattern, content, re.VERBOSE | re.MULTILINE):
func_name = match.group(3)
# Skip if it's inside a class
if not self._is_top_level(content, match.start()):
continue
return_type = match.group(1) or match.group(2)
params = match.group(4)
span = content[match.start() : match.end()]
is_async = "async" in span
is_generator = "*" in span
func_info = FunctionInfo(
name=func_name,
line=content[: match.start()].count("\n") + 1,
return_type=return_type,
parameters=self._parse_parameters(params),
is_async=is_async,
is_generator=is_generator,
is_private=func_name.startswith("_"),
)
structure.functions.append(func_info)
# Extract enums (brace-aware, supports enhanced enums with methods)
enum_head_pattern = r"^\s*enum\s+(\w+)(?:\s*<[^>]+>)?(?:\s+implements\s+[^\{]+)?\s*\{"
for m in re.finditer(enum_head_pattern, content, re.MULTILINE):
enum_name = m.group(1)
enum_body = self._extract_block(content, m.end()) or ""
if enum_body is None:
continue
# Determine the values section: up to first top-level ';' if present
values_part = enum_body
depth = 0
cutoff = None
for i, ch in enumerate(enum_body):
if ch == "{":
depth += 1
elif ch == "}":
depth = max(0, depth - 1)
elif ch == ";" and depth == 0:
cutoff = i
break
if cutoff is not None:
values_part = enum_body[:cutoff]
values = self._parse_enum_values(values_part)
structure.enums.append(
{
"name": enum_name,
"line": content[: m.start()].count("\n") + 1,
"values": values,
"has_enhanced_features": ("(" in values_part) or (cutoff is not None),
}
)
# Extract extensions
extension_pattern = r"^\s*extension\s+(?:(\w+)\s+)?on\s+([^\{]+)\s*\{"
for match in re.finditer(extension_pattern, content, re.MULTILINE):
extension_name = match.group(1) or f"on {match.group(2)}"
on_type = match.group(2).strip()
structure.extensions.append(
{
"name": extension_name,
"line": content[: match.start()].count("\n") + 1,
"on_type": on_type,
}
)
# Extract typedefs
typedef_pattern = r"^\s*typedef\s+(\w+)(?:<[^>]+>)?\s*=\s*([^;]+);"
for match in re.finditer(typedef_pattern, content, re.MULTILINE):
structure.typedefs.append(
{
"name": match.group(1),
"line": content[: match.start()].count("\n") + 1,
"definition": match.group(2).strip(),
}
)
# Count null safety features
structure.nullable_types = len(re.findall(r"\w+\?(?:\s|,|\))", content))
structure.null_assertions = len(re.findall(r"\w+!(?:\.|;|\s|\))", content))
structure.late_variables = len(re.findall(r"\blate\s+", content))
structure.null_aware_operators = len(re.findall(r"\?\?|\?\.", content))
# Count async features
structure.async_functions = len(re.findall(r"\basync\s*(?:\*)?\s*(?:=>|\{)", content))
structure.await_expressions = len(re.findall(r"\bawait\s+", content))
structure.future_count = len(re.findall(r"\bFuture(?:\s*<|[.(])", content))
structure.stream_count = len(re.findall(r"\bStream(?:\s*<|[.(])", content))
# Detect test file
structure.is_test_file = (
"_test.dart" in file_path.name or file_path.parts and "test" in file_path.parts
)
# Detect main function
structure.has_main = bool(re.search(r"\bvoid\s+main\s*\(", content))
return structure
calculate_complexity¶
Calculate complexity metrics for Dart code.
Calculates: - Cyclomatic complexity - Cognitive complexity - Null safety complexity - Async complexity - Flutter-specific complexity - Class hierarchy depth
| PARAMETER | DESCRIPTION |
|---|---|
content | Dart source code TYPE: |
file_path | Path to the file being analyzed TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
ComplexityMetrics | ComplexityMetrics object with calculated metrics |
Source code in tenets/core/analysis/implementations/dart_analyzer.py
def calculate_complexity(self, content: str, file_path: Path) -> ComplexityMetrics:
"""Calculate complexity metrics for Dart code.
Calculates:
- Cyclomatic complexity
- Cognitive complexity
- Null safety complexity
- Async complexity
- Flutter-specific complexity
- Class hierarchy depth
Args:
content: Dart source code
file_path: Path to the file being analyzed
Returns:
ComplexityMetrics object with calculated metrics
"""
metrics = ComplexityMetrics()
# Calculate cyclomatic complexity
complexity = 1
decision_keywords = [
r"\bif\b",
r"\belse\s+if\b",
r"\belse\b",
r"\bfor\b",
r"\bwhile\b",
r"\bdo\b",
r"\bswitch\b",
r"\bcase\b",
r"\bcatch\b",
r"\b\?\s*[^:]+\s*:", # Ternary operator
r"\?\?", # Null coalescing
r"&&",
r"\|\|",
]
for keyword in decision_keywords:
complexity += len(re.findall(keyword, content))
metrics.cyclomatic = complexity
# Calculate cognitive complexity
cognitive = 0
nesting_level = 0
max_nesting = 0
lines = content.split("\n")
for line in lines:
# Skip comments
if line.strip().startswith("//"):
continue
# Track nesting
opening_braces = line.count("{")
closing_braces = line.count("}")
nesting_level += opening_braces - closing_braces
max_nesting = max(max_nesting, nesting_level)
# Control structures with nesting penalty
control_patterns = [
(r"\bif\b", 1),
(r"\belse\s+if\b", 1),
(r"\belse\b", 0),
(r"\bfor\b", 1),
(r"\bwhile\b", 1),
(r"\bdo\b", 1),
(r"\bswitch\b", 1),
(r"\btry\b", 1),
(r"\bcatch\b", 1),
]
for pattern, weight in control_patterns:
if re.search(pattern, line):
cognitive += weight * (1 + max(0, nesting_level - 1))
metrics.cognitive = cognitive
metrics.max_depth = max_nesting
# Count code elements
metrics.line_count = len(lines)
metrics.code_lines = len([l for l in lines if l.strip() and not l.strip().startswith("//")])
metrics.comment_lines = len([l for l in lines if l.strip().startswith("//")])
metrics.comment_ratio = (
metrics.comment_lines / metrics.line_count if metrics.line_count > 0 else 0
)
# Count classes and methods
metrics.class_count = len(re.findall(r"\bclass\s+\w+", content))
metrics.mixin_count = len(re.findall(r"\bmixin\s+\w+", content))
metrics.method_count = len(
re.findall(
r"(?:^|\s)(?:Future|Stream|void|[\w<>]+)\s+\w+\s*\([^)]*\)\s*(?:async\s*)?(?:=>|\{)",
content,
)
)
# Null safety metrics
metrics.nullable_types = len(re.findall(r"\w+\?(?:\s|,|\))", content))
metrics.null_assertions = len(re.findall(r"\w+!(?:\.|;|\s|\))", content))
metrics.late_keywords = len(re.findall(r"\blate\s+", content))
metrics.null_aware_ops = len(re.findall(r"\?\?|\?\.|\?\.\?", content))
metrics.required_keywords = len(re.findall(r"\brequired\s+", content))
# Async metrics
metrics.async_functions = len(re.findall(r"\basync\s*(?:\*)?\s*(?:=>|\{)", content))
metrics.await_count = len(re.findall(r"\bawait\s+", content))
metrics.future_count = len(re.findall(r"\bFuture(?:\s*<|[.(])", content))
metrics.stream_count = len(re.findall(r"\bStream(?:\s*<|[.(])", content))
metrics.completer_count = len(re.findall(r"\bCompleter<", content))
# Flutter-specific metrics
if self._is_flutter_file(content):
metrics.widget_count = len(re.findall(r"\bWidget\b", content))
metrics.build_methods = len(re.findall(r"\bWidget\s+build\s*\(", content))
metrics.setstate_calls = len(re.findall(r"\bsetState\s*\(", content))
metrics.stateful_widgets = len(re.findall(r"extends\s+StatefulWidget", content))
metrics.stateless_widgets = len(re.findall(r"extends\s+StatelessWidget", content))
metrics.inherited_widgets = len(re.findall(r"extends\s+InheritedWidget", content))
# Flutter hooks and keys
metrics.keys_used = len(
re.findall(r"\bKey\s*\(|GlobalKey|ValueKey|ObjectKey|UniqueKey", content)
)
metrics.context_usage = len(re.findall(r"\bBuildContext\b", content))
# Exception handling metrics
metrics.try_blocks = len(re.findall(r"\btry\s*\{", content))
metrics.catch_blocks = len(re.findall(r"\bcatch\s*\(", content))
metrics.finally_blocks = len(re.findall(r"\bfinally\s*\{", content))
metrics.throw_statements = len(re.findall(r"\bthrow\s+", content))
metrics.rethrow_statements = len(re.findall(r"\brethrow\s*;", content))
# Type system metrics
metrics.generic_types = len(re.findall(r"<[\w\s,<>]+>", content))
metrics.type_parameters = len(re.findall(r"<\w+(?:\s+extends\s+\w+)?>", content))
metrics.dynamic_types = len(re.findall(r"\bdynamic\b", content))
metrics.var_declarations = len(re.findall(r"\bvar\s+\w+", content))
# Calculate maintainability index
import math
if metrics.code_lines > 0:
# Adjusted for Dart
null_safety_factor = 1 - (metrics.null_assertions * 0.01)
async_factor = 1 - (metrics.async_functions * 0.01)
flutter_factor = (
1 - (metrics.setstate_calls * 0.02) if hasattr(metrics, "setstate_calls") else 1
)
type_factor = 1 + ((metrics.nullable_types - metrics.dynamic_types) * 0.001)
mi = (
171
- 5.2 * math.log(max(1, complexity))
- 0.23 * complexity
- 16.2 * math.log(metrics.code_lines)
+ 10 * null_safety_factor
+ 5 * async_factor
+ 5 * flutter_factor
+ 5 * type_factor
)
metrics.maintainability_index = max(0, min(100, mi))
return metrics
GDScriptAnalyzer¶
Bases: LanguageAnalyzer
GDScript code analyzer for Godot development.
Provides comprehensive analysis for GDScript files including: - Preload and load statements - Class inheritance (extends) - Signal declarations and connections - Export variable declarations - Onready variables and node references - Godot lifecycle methods (_ready, _process, etc.) - Tool scripts and custom resources - Typed GDScript (static typing) - Inner classes - Setget properties - Remote and master/puppet keywords (networking)
Supports Godot 3.x and 4.x GDScript syntax.
Initialize the GDScript analyzer with logger.
Source code in tenets/core/analysis/implementations/gdscript_analyzer.py
extract_imports¶
Extract preload, load, and class references from GDScript code.
Handles: - preload statements: preload("res://path/to/script.gd") - load statements: load("res://path/to/resource.tres") - const preloads: const MyClass = preload("res://MyClass.gd") - class_name declarations (Godot 3.1+) - Tool script declarations
| PARAMETER | DESCRIPTION |
|---|---|
content | GDScript source code TYPE: |
file_path | Path to the file being analyzed TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
List[ImportInfo] | List of ImportInfo objects with import details |
Source code in tenets/core/analysis/implementations/gdscript_analyzer.py
def extract_imports(self, content: str, file_path: Path) -> List[ImportInfo]:
"""Extract preload, load, and class references from GDScript code.
Handles:
- preload statements: preload("res://path/to/script.gd")
- load statements: load("res://path/to/resource.tres")
- const preloads: const MyClass = preload("res://MyClass.gd")
- class_name declarations (Godot 3.1+)
- Tool script declarations
Args:
content: GDScript source code
file_path: Path to the file being analyzed
Returns:
List of ImportInfo objects with import details
"""
imports = []
lines = content.split("\n")
for i, line in enumerate(lines, 1):
# Skip comments
if line.strip().startswith("#"):
continue
# Preload statements
preload_pattern = r'(?:const\s+)?(\w+)?\s*=?\s*preload\s*\(\s*["\']([^"\']+)["\']\s*\)'
# Use finditer to support multiple preloads on a single line and avoid overlapping matches
for match in re.finditer(preload_pattern, line):
const_name = match.group(1)
resource_path = match.group(2)
imports.append(
ImportInfo(
module=resource_path,
alias=const_name,
line=i,
type="preload",
is_relative=resource_path.startswith("res://")
or resource_path.startswith("user://"),
is_resource=True,
resource_type=self._detect_resource_type(resource_path),
)
)
# Load statements (ensure we don't match the 'load' in 'preload')
load_pattern = r"(?<!\w)load\s*\("
for match in re.finditer(load_pattern, line):
# Extract the actual path argument following this 'load('
path_match = re.search(r'\(\s*["\']([^"\']+)["\']\s*\)', line[match.start() :])
if not path_match:
continue
resource_path = path_match.group(1)
imports.append(
ImportInfo(
module=resource_path,
line=i,
type="load",
is_relative=resource_path.startswith("res://")
or resource_path.startswith("user://"),
is_runtime_load=True,
resource_type=self._detect_resource_type(resource_path),
)
)
# Class inheritance (extends)
extends_pattern = r'^\s*extends\s+["\']?([^"\'\s]+)["\']?'
match = re.match(extends_pattern, line)
if match:
parent_class = match.group(1)
# Check if it's a path or class name
is_path = "/" in parent_class or parent_class.endswith(".gd")
imports.append(
ImportInfo(
module=parent_class,
line=i,
type="extends",
is_relative=is_path,
is_inheritance=True,
parent_type="script" if is_path else "class",
)
)
# Class_name declarations (for autoload/global classes)
class_name_pattern = r'^\s*class_name\s+(\w+)(?:\s*,\s*["\']([^"\']+)["\'])?'
match = re.match(class_name_pattern, line)
if match:
class_name = match.group(1)
icon_path = match.group(2)
if icon_path:
imports.append(
ImportInfo(
module=icon_path,
line=i,
type="icon",
is_relative=True,
is_resource=True,
associated_class=class_name,
)
)
# Check for tool script declaration
if re.search(r"^\s*tool\s*$", content, re.MULTILINE):
imports.append(
ImportInfo(
module="@tool",
line=1,
type="tool_mode",
is_relative=False,
is_editor_script=True,
)
)
# Check for @tool annotation (Godot 4.x)
if re.search(r"^\s*@tool\s*$", content, re.MULTILINE):
imports.append(
ImportInfo(
module="@tool",
line=1,
type="annotation",
is_relative=False,
is_editor_script=True,
)
)
return imports
extract_exports¶
Extract exported symbols from GDScript code.
In GDScript, exports include: - class_name declarations (global classes) - export variables - signals - Public functions (by convention, non-underscore prefixed)
| PARAMETER | DESCRIPTION |
|---|---|
content | GDScript source code TYPE: |
file_path | Path to the file being analyzed TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
List[Dict[str, Any]] | List of exported symbols |
Source code in tenets/core/analysis/implementations/gdscript_analyzer.py
def extract_exports(self, content: str, file_path: Path) -> List[Dict[str, Any]]:
"""Extract exported symbols from GDScript code.
In GDScript, exports include:
- class_name declarations (global classes)
- export variables
- signals
- Public functions (by convention, non-underscore prefixed)
Args:
content: GDScript source code
file_path: Path to the file being analyzed
Returns:
List of exported symbols
"""
exports = []
# Extract class_name (makes class globally accessible)
class_name_pattern = r'^\s*class_name\s+(\w+)(?:\s*,\s*["\']([^"\']+)["\'])?'
match = re.search(class_name_pattern, content, re.MULTILINE)
if match:
exports.append(
{
"name": match.group(1),
"type": "global_class",
"line": content[: match.start()].count("\n") + 1,
"icon": match.group(2),
"is_autoload_candidate": True,
}
)
# Extract exported variables (Godot 3.x syntax)
export_var_pattern = r"^\s*export(?:\s*\(([^)]*)\))?\s+(?:var\s+)?(\w+)"
for match in re.finditer(export_var_pattern, content, re.MULTILINE):
export_type = match.group(1)
var_name = match.group(2)
exports.append(
{
"name": var_name,
"type": "export_var",
"line": content[: match.start()].count("\n") + 1,
"export_type": export_type,
"inspector_visible": True,
}
)
# Extract exported variables (Godot 4.x syntax with @export)
# Allow optional annotation arguments e.g., @export_range(0,1)
export_annotation_pattern = r"^\s*@export(?:_([a-z_]+))?(?:\([^)]*\))?\s+(?:var\s+)?(\w+)"
for match in re.finditer(export_annotation_pattern, content, re.MULTILINE):
export_modifier = match.group(1)
var_name = match.group(2)
exports.append(
{
"name": var_name,
"type": "export_var",
"line": content[: match.start()].count("\n") + 1,
"export_modifier": export_modifier,
"inspector_visible": True,
"godot_version": 4,
}
)
# Extract signals
signal_pattern = r"^\s*signal\s+(\w+)\s*(?:\(([^)]*)\))?"
for match in re.finditer(signal_pattern, content, re.MULTILINE):
signal_name = match.group(1)
parameters = match.group(2)
exports.append(
{
"name": signal_name,
"type": "signal",
"line": content[: match.start()].count("\n") + 1,
"parameters": self._parse_signal_parameters(parameters),
"is_event": True,
}
)
# Extract public functions (non-underscore prefixed)
func_pattern = r"^\s*(?:static\s+)?func\s+([a-zA-Z]\w*)\s*\("
for match in re.finditer(func_pattern, content, re.MULTILINE):
func_name = match.group(1)
exports.append(
{
"name": func_name,
"type": "function",
"line": content[: match.start()].count("\n") + 1,
"is_public": True,
"is_static": "static" in match.group(0),
}
)
# Extract enums
enum_pattern = r"^\s*enum\s+(\w+)\s*\{"
for match in re.finditer(enum_pattern, content, re.MULTILINE):
exports.append(
{
"name": match.group(1),
"type": "enum",
"line": content[: match.start()].count("\n") + 1,
}
)
# Extract constants (often used as exports in GDScript)
const_pattern = r"^\s*const\s+([A-Z][A-Z0-9_]*)\s*="
for match in re.finditer(const_pattern, content, re.MULTILINE):
exports.append(
{
"name": match.group(1),
"type": "constant",
"line": content[: match.start()].count("\n") + 1,
"is_public": True,
}
)
return exports
extract_structure¶
Extract code structure from GDScript file.
Extracts: - Class inheritance and structure - Inner classes - Functions with type hints - Godot lifecycle methods - Signals and their connections - Export variables - Onready variables - Node references - Setget properties - Enums and constants
| PARAMETER | DESCRIPTION |
|---|---|
content | GDScript source code TYPE: |
file_path | Path to the file being analyzed TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
CodeStructure | CodeStructure object with extracted elements |
Source code in tenets/core/analysis/implementations/gdscript_analyzer.py
def extract_structure(self, content: str, file_path: Path) -> CodeStructure:
"""Extract code structure from GDScript file.
Extracts:
- Class inheritance and structure
- Inner classes
- Functions with type hints
- Godot lifecycle methods
- Signals and their connections
- Export variables
- Onready variables
- Node references
- Setget properties
- Enums and constants
Args:
content: GDScript source code
file_path: Path to the file being analyzed
Returns:
CodeStructure object with extracted elements
"""
structure = CodeStructure()
# Detect if it's a tool script
structure.is_tool_script = bool(re.search(r"^\s*(?:@)?tool\s*$", content, re.MULTILINE))
# Extract class name
class_name_match = re.search(r"^\s*class_name\s+(\w+)", content, re.MULTILINE)
if class_name_match:
structure.class_name = class_name_match.group(1)
# Extract parent class
extends_match = re.search(r'^\s*extends\s+["\']?([^"\'\s]+)["\']?', content, re.MULTILINE)
if extends_match:
structure.parent_class = extends_match.group(1)
# Detect Godot version (4.x uses @annotations)
structure.godot_version = (
4 if re.search(r"^\s*@(export|onready|tool)", content, re.MULTILINE) else 3
)
# Extract main class info
main_class = ClassInfo(
name=getattr(structure, "class_name", None) or file_path.stem,
line=1,
bases=(
[getattr(structure, "parent_class", None)]
if getattr(structure, "parent_class", None)
else []
),
)
# Extract functions
func_pattern = r"^\s*(?:static\s+)?func\s+(\w+)\s*\(([^)]*)\)(?:\s*->\s*([^:]+))?:"
for match in re.finditer(func_pattern, content, re.MULTILINE):
func_name = match.group(1)
params = match.group(2)
return_type = match.group(3)
is_private = func_name.startswith("_")
is_lifecycle = self._is_lifecycle_method(func_name)
is_virtual = func_name.startswith("_") and not func_name.startswith("__")
func_info = FunctionInfo(
name=func_name,
line=content[: match.start()].count("\n") + 1,
parameters=self._parse_function_parameters(params),
return_type=return_type.strip() if return_type else None,
is_private=is_private,
is_lifecycle=is_lifecycle,
is_virtual=is_virtual,
is_static="static" in content[match.start() - 20 : match.start()],
)
structure.functions.append(func_info)
main_class.methods.append(
{
"name": func_name,
"visibility": "private" if is_private else "public",
"is_lifecycle": is_lifecycle,
}
)
# Extract inner classes
inner_class_pattern = r"^\s*class\s+(\w+)(?:\s+extends\s+([^:]+))?:"
for match in re.finditer(inner_class_pattern, content, re.MULTILINE):
inner_class = ClassInfo(
name=match.group(1),
line=content[: match.start()].count("\n") + 1,
bases=[match.group(2).strip()] if match.group(2) else [],
is_inner=True,
)
structure.classes.append(inner_class)
# Add main class
structure.classes.insert(0, main_class)
# Extract signals
signal_pattern = r"^\s*signal\s+(\w+)\s*(?:\(([^)]*)\))?"
for match in re.finditer(signal_pattern, content, re.MULTILINE):
structure.signals.append(
{
"name": match.group(1),
"line": content[: match.start()].count("\n") + 1,
"parameters": self._parse_signal_parameters(match.group(2)),
}
)
# Extract export variables
# Godot 3.x
export_pattern = r"^\s*export(?:\s*\(([^)]*)\))?\s+(?:var\s+)?(\w+)(?:\s*:\s*([^=\n]+))?(?:\s*=\s*([^\n]+))?"
for match in re.finditer(export_pattern, content, re.MULTILINE):
structure.export_vars.append(
{
"name": match.group(2),
"export_hint": match.group(1),
"type": match.group(3).strip() if match.group(3) else None,
"default": match.group(4).strip() if match.group(4) else None,
"line": content[: match.start()].count("\n") + 1,
}
)
# Godot 4.x
export_4_pattern = r"^\s*@export(?:_([a-z_]+))?(?:\([^)]*\))?\s+(?:var\s+)?(\w+)(?:\s*:\s*([^=\n]+))?(?:\s*=\s*([^\n]+))?"
for match in re.finditer(export_4_pattern, content, re.MULTILINE):
structure.export_vars.append(
{
"name": match.group(2),
"export_modifier": match.group(1),
"type": match.group(3).strip() if match.group(3) else None,
"default": match.group(4).strip() if match.group(4) else None,
"line": content[: match.start()].count("\n") + 1,
"godot_4": True,
}
)
# Extract onready variables
# Godot 3.x
onready_pattern = r"^\s*onready\s+var\s+(\w+)(?:\s*:\s*([^=\n]+))?\s*=\s*([^\n]+)"
for match in re.finditer(onready_pattern, content, re.MULTILINE):
var_name = match.group(1)
var_type = match.group(2)
initialization = match.group(3)
# Check if it's a node reference
is_node_ref = bool(re.search(r"(?:\$|get_node)", initialization))
node_path = self._extract_node_path(initialization)
structure.onready_vars.append(
{
"name": var_name,
"type": var_type.strip() if var_type else None,
"initialization": initialization.strip(),
"is_node_ref": is_node_ref,
"node_path": node_path,
"line": content[: match.start()].count("\n") + 1,
}
)
# Godot 4.x
onready_4_pattern = r"^\s*@onready\s+var\s+(\w+)(?:\s*:\s*([^=\n]+))?\s*=\s*([^\n]+)"
for match in re.finditer(onready_4_pattern, content, re.MULTILINE):
var_name = match.group(1)
var_type = match.group(2)
initialization = match.group(3)
is_node_ref = bool(re.search(r"(?:\$|get_node)", initialization))
node_path = self._extract_node_path(initialization)
structure.onready_vars.append(
{
"name": var_name,
"type": var_type.strip() if var_type else None,
"initialization": initialization.strip(),
"is_node_ref": is_node_ref,
"node_path": node_path,
"line": content[: match.start()].count("\n") + 1,
"godot_4": True,
}
)
# Extract regular variables
var_pattern = r"^\s*var\s+(\w+)(?:\s*:\s*([^=\n]+))?(?:\s*=\s*([^\n]+))?"
for match in re.finditer(var_pattern, content, re.MULTILINE):
# Skip if it's an export or onready var
line_start = content[: match.start()].rfind("\n") + 1
line_content = content[line_start : match.end()]
if "export" in line_content or "onready" in line_content or "@" in line_content:
continue
structure.variables.append(
{
"name": match.group(1),
"type": match.group(2).strip() if match.group(2) else None,
"initial_value": match.group(3).strip() if match.group(3) else None,
"line": content[: match.start()].count("\n") + 1,
}
)
# Extract constants
const_pattern = r"^\s*const\s+(\w+)(?:\s*:\s*([^=\n]+))?\s*=\s*([^\n]+)"
for match in re.finditer(const_pattern, content, re.MULTILINE):
structure.constants.append(
{
"name": match.group(1),
"type": match.group(2).strip() if match.group(2) else None,
"value": match.group(3).strip(),
"line": content[: match.start()].count("\n") + 1,
}
)
# Extract enums
enum_pattern = r"^\s*enum\s+(\w+)\s*\{([^}]+)\}"
for match in re.finditer(enum_pattern, content, re.MULTILINE):
enum_name = match.group(1)
enum_body = match.group(2)
values = self._parse_enum_values(enum_body)
structure.enums.append(
{
"name": enum_name,
"values": values,
"line": content[: match.start()].count("\n") + 1,
}
)
# Extract setget properties
# Support optional setter/getter and missing entries: e.g., setget set_mana or setget , get_level
setget_pattern = r"^\s*var\s+(\w+)(?:[^=\n]*=\s*[^\n]+)?\s+setget\s*(?:([A-Za-z_]\w*)\s*)?(?:,\s*([A-Za-z_]\w*)\s*)?"
for match in re.finditer(setget_pattern, content, re.MULTILINE):
structure.setget_properties.append(
{
"name": match.group(1),
"setter": match.group(2) if match.group(2) else None,
"getter": match.group(3) if match.group(3) else None,
"line": content[: match.start()].count("\n") + 1,
}
)
# Count node references
structure.node_references = len(re.findall(r'\$["\']?[^"\'\s]+["\']?', content))
structure.get_node_calls = len(re.findall(r"get_node\s*\(", content))
# Count signal connections (method form and free function form)
structure.connect_calls = len(re.findall(r"\.connect\s*\(|(?<!\.)\bconnect\s*\(", content))
structure.emit_signal_calls = len(re.findall(r"emit_signal\s*\(", content))
# Detect if it's a custom resource
structure.is_custom_resource = bool(
structure.parent_class and "Resource" in structure.parent_class
)
# Detect if it's an editor plugin
structure.is_editor_plugin = bool(
structure.parent_class and "EditorPlugin" in structure.parent_class
)
return structure
calculate_complexity¶
Calculate complexity metrics for GDScript code.
Calculates: - Cyclomatic complexity - Cognitive complexity - Godot-specific complexity (signals, exports, node references) - Nesting depth - Function count and complexity distribution
| PARAMETER | DESCRIPTION |
|---|---|
content | GDScript source code TYPE: |
file_path | Path to the file being analyzed TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
ComplexityMetrics | ComplexityMetrics object with calculated metrics |
Source code in tenets/core/analysis/implementations/gdscript_analyzer.py
def calculate_complexity(self, content: str, file_path: Path) -> ComplexityMetrics:
"""Calculate complexity metrics for GDScript code.
Calculates:
- Cyclomatic complexity
- Cognitive complexity
- Godot-specific complexity (signals, exports, node references)
- Nesting depth
- Function count and complexity distribution
Args:
content: GDScript source code
file_path: Path to the file being analyzed
Returns:
ComplexityMetrics object with calculated metrics
"""
metrics = ComplexityMetrics()
# Calculate cyclomatic complexity
complexity = 1
decision_keywords = [
r"\bif\b",
r"\belif\b",
r"\belse\b",
r"\bfor\b",
r"\bwhile\b",
r"\bmatch\b",
r"\bwhen\b",
r"\band\b",
r"\bor\b",
]
for keyword in decision_keywords:
complexity += len(re.findall(keyword, content))
# Each match-case branch contributes to complexity; count simple case labels (numbers, strings, or underscore)
case_label_pattern = r"^\s*(?:_|-?\d+|\"[^\"\n]+\"|\'[^\'\n]+\')\s*:"
complexity += len(re.findall(case_label_pattern, content, re.MULTILINE))
# Inline lambda expressions (func(...) :) add decision/branching potential
lambda_inline_pattern = (
r"func\s*\(" # named functions are 'func name(', lambdas are 'func(' directly
)
complexity += len(re.findall(lambda_inline_pattern, content))
metrics.cyclomatic = complexity
# Calculate cognitive complexity
cognitive = 0
nesting_level = 0
max_nesting = 0
lines = content.split("\n")
for line in lines:
# Skip comments
if line.strip().startswith("#"):
continue
# Track nesting by indentation (GDScript uses indentation)
if line.strip():
indent = len(line) - len(line.lstrip())
# Assuming tab or 4 spaces as one level
if "\t" in line[:indent]:
current_level = line[:indent].count("\t")
else:
current_level = indent // 4
max_nesting = max(max_nesting, current_level)
# Control structures with nesting penalty
control_patterns = [
(r"\bif\b", 1),
(r"\belif\b", 1),
(r"\belse\b", 0),
(r"\bfor\b", 1),
(r"\bwhile\b", 1),
(r"\bmatch\b", 1),
]
for pattern, weight in control_patterns:
if re.search(pattern, line):
cognitive += weight * (1 + max(0, current_level))
metrics.cognitive = cognitive
metrics.max_depth = max_nesting
# Count code elements
metrics.line_count = len(lines)
metrics.code_lines = len([l for l in lines if l.strip() and not l.strip().startswith("#")])
metrics.comment_lines = len([l for l in lines if l.strip().startswith("#")])
metrics.comment_ratio = (
metrics.comment_lines / metrics.line_count if metrics.line_count > 0 else 0
)
# Count functions
metrics.function_count = len(re.findall(r"\bfunc\s+\w+", content))
# Count classes
metrics.class_count = len(re.findall(r"\bclass\s+\w+", content))
metrics.class_count += 1 if re.search(r"^\s*extends\s+", content, re.MULTILINE) else 0
# Godot-specific metrics
metrics.signal_count = len(re.findall(r"\bsignal\s+\w+", content))
metrics.export_count = len(re.findall(r"(?:@)?export(?:_\w+)?(?:\([^)]*\))?\s+", content))
metrics.onready_count = len(re.findall(r"(?:@)?onready\s+var", content))
# Node reference metrics
metrics.node_ref_count = len(re.findall(r'\$["\']?[^"\'\s]+["\']?', content))
metrics.get_node_count = len(re.findall(r"get_node\s*\(", content))
# Signal connection metrics
metrics.connect_count = len(re.findall(r"\.connect\s*\(|(?<!\.)\bconnect\s*\(", content))
metrics.emit_count = len(re.findall(r"emit_signal\s*\(", content))
# Lifecycle method count
lifecycle_methods = [
"_ready",
"_enter_tree",
"_exit_tree",
"_process",
"_physics_process",
"_input",
"_unhandled_input",
"_draw",
"_gui_input",
"_notification",
]
metrics.lifecycle_count = sum(
1 for method in lifecycle_methods if re.search(rf"\bfunc\s+{method}\s*\(", content)
)
# RPC/Networking metrics
metrics.rpc_count = len(
re.findall(r"@rpc|rpc\(|rpc_unreliable\(|remotesync\s+func", content)
)
# Type hints metrics
metrics.typed_vars = len(re.findall(r"(?:var|const)\s+\w+\s*:\s*\w+", content))
metrics.typed_funcs = len(re.findall(r"func\s+\w+\s*\([^)]*:\s*\w+[^)]*\)", content))
metrics.return_types = len(re.findall(r"\)\s*->\s*\w+\s*:", content))
# Calculate Godot-specific complexity score
godot_complexity = (
metrics.signal_count * 2
+ metrics.export_count
+ metrics.onready_count
+ metrics.node_ref_count * 0.5
+ metrics.connect_count * 2
+ metrics.emit_count
)
# Calculate maintainability index
import math
if metrics.code_lines > 0:
# Adjusted for GDScript
godot_factor = 1 - (godot_complexity * 0.001)
type_factor = 1 + (metrics.typed_vars + metrics.typed_funcs) * 0.001
mi = (
171
- 5.2 * math.log(max(1, complexity))
- 0.23 * complexity
- 16.2 * math.log(metrics.code_lines)
+ 10 * godot_factor
+ 5 * type_factor
)
metrics.maintainability_index = max(0, min(100, mi))
return metrics
GenericAnalyzer¶
Bases: LanguageAnalyzer
Generic analyzer for unsupported file types.
Provides basic analysis for text-based files including: - Line and character counting - Basic pattern matching for imports/includes - Simple complexity estimation - Keyword extraction - Configuration file parsing (JSON, YAML, XML, etc.)
This analyzer serves as a fallback for files without specific language support and can handle various text formats.
Initialize the generic analyzer with logger.
Source code in tenets/core/analysis/implementations/generic_analyzer.py
extract_imports¶
Extract potential imports/includes from generic text.
Looks for common import patterns across various languages and configuration files.
| PARAMETER | DESCRIPTION |
|---|---|
content | File content TYPE: |
file_path | Path to the file being analyzed TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
List[ImportInfo] | List of ImportInfo objects with detected imports |
Source code in tenets/core/analysis/implementations/generic_analyzer.py
def extract_imports(self, content: str, file_path: Path) -> List[ImportInfo]:
"""Extract potential imports/includes from generic text.
Looks for common import patterns across various languages
and configuration files.
Args:
content: File content
file_path: Path to the file being analyzed
Returns:
List of ImportInfo objects with detected imports
"""
imports = []
lines = content.split("\n")
# Common import/include patterns
patterns = [
# Include patterns (C-style, various scripting languages)
(r"^\s*#include\s+<([^>]+)>", "include"), # angle includes
(r'^\s*#include\s+"([^"]+)"', "include"), # quote includes
(r"^\s*include\s+[\'\"]([^\'\"]+)[\'\"]", "include"),
# CMake include()
(r"^\s*include\s*\(\s*([^)\s]+)\s*\)", "include"),
# Import patterns (various languages)
(r'^\s*import\s+[\'"]([^\'"]+)[\'"]', "import"), # import "module"
(r"^\s*import\s+([A-Za-z_][\w\.]*)\b", "import"), # import os
(r'^\s*from\s+[\'"]([^\'"]+)[\'"]', "from"), # from "mod"
(r"^\s*from\s+([A-Za-z_][\w\.]*)\s+import\b", "from"), # from pkg import X
(r'^\s*require\s+[\'"]([^\'"]+)[\'"]', "require"),
# PHP/Perl and JS style use statements
(r"^\s*use\s+([\\\w:]+);?", "use"), # use Data::Dumper; or use Foo\Bar;
# Load/source patterns (shell scripts)
(r'^\s*source\s+[\'"]?([^\'"]+)[\'"]?', "source"),
(r'^\s*\.[ \t]+[\'"]?([^\'"]+)[\'"]?', "source"),
# Configuration file references
(r'[\'"]?(?:file|path|src|href|url)[\'"]?\s*[:=]\s*[\'"]([^\'"]+)[\'"]', "reference"),
]
captured_modules: set[str] = set()
for i, line in enumerate(lines, 1):
# Skip comments (generic comment patterns) but keep C preprocessor includes
if (
line.strip().startswith("#") and not re.match(r"^\s*#include\b", line)
) or line.strip().startswith("//"):
continue
for pattern, import_type in patterns:
match = re.search(pattern, line, re.IGNORECASE)
if match:
module = match.group(1)
imports.append(
ImportInfo(
module=module,
line=i,
type=import_type,
is_relative=self._is_relative_path(module),
)
)
captured_modules.add(module)
break
# Special case: 'use strict;' (JavaScript directive)
if re.match(r"^\s*use\s+strict\s*;?\s*$", line):
imports.append(ImportInfo(module="strict", line=i, type="use", is_relative=False))
captured_modules.add("strict")
# Special handling for specific file types
if file_path.suffix.lower() in [".json", ".yaml", ".yml"]:
imports.extend(self._extract_config_dependencies(content, file_path))
# Detect standalone file references like config.yml in logs
file_ref_pattern = re.compile(
r"\b([\w./-]+\.(?:ya?ml|json|conf|cfg|ini|xml|toml|log|txt|sh))\b"
)
for i, line in enumerate(lines, 1):
for m in file_ref_pattern.finditer(line):
module = m.group(1)
if module not in captured_modules:
imports.append(
ImportInfo(
module=module,
line=i,
type="reference",
is_relative=self._is_relative_path(module),
)
)
captured_modules.add(module)
return imports
extract_exports¶
Extract potential exports from generic text.
Looks for common export patterns and definitions.
| PARAMETER | DESCRIPTION |
|---|---|
content | File content TYPE: |
file_path | Path to the file being analyzed TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
List[Dict[str, Any]] | List of potential exported symbols |
Source code in tenets/core/analysis/implementations/generic_analyzer.py
def extract_exports(self, content: str, file_path: Path) -> List[Dict[str, Any]]:
"""Extract potential exports from generic text.
Looks for common export patterns and definitions.
Args:
content: File content
file_path: Path to the file being analyzed
Returns:
List of potential exported symbols
"""
exports = []
# Common export/definition patterns
patterns = [
# Function-like definitions
(r"^(?:function|def|func|sub|proc)\s+(\w+)", "function"),
(r"^(\w+)\s*\(\)\s*\{", "function"),
# Class-like definitions
(r"^(?:class|struct|type|interface)\s+(\w+)", "class"),
# Variable/constant definitions
(r"^(?:export\s+)?(?:const|let|var|val)\s+(\w+)\s*=", "variable"),
(r'^(\w+)\s*=\s*[\'"]?[^\'"\n]+[\'"]?', "assignment"),
# Export statements
(r"^export\s+(\w+)", "export"),
(r"^module\.exports\.(\w+)", "export"),
]
for pattern, export_type in patterns:
for match in re.finditer(pattern, content, re.MULTILINE):
name = match.group(1)
exports.append(
{
"name": name,
"type": export_type,
"line": content[: match.start()].count("\n") + 1,
}
)
# For configuration files, extract top-level keys
if file_path.suffix.lower() in [".json", ".yaml", ".yml", ".toml", ".ini"]:
exports.extend(self._extract_config_keys(content, file_path))
return exports
extract_structure¶
Extract basic structure from generic text.
Attempts to identify structural elements using pattern matching and indentation analysis.
| PARAMETER | DESCRIPTION |
|---|---|
content | File content TYPE: |
file_path | Path to the file being analyzed TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
CodeStructure | CodeStructure object with detected elements |
Source code in tenets/core/analysis/implementations/generic_analyzer.py
def extract_structure(self, content: str, file_path: Path) -> CodeStructure:
"""Extract basic structure from generic text.
Attempts to identify structural elements using pattern matching
and indentation analysis.
Args:
content: File content
file_path: Path to the file being analyzed
Returns:
CodeStructure object with detected elements
"""
structure = CodeStructure()
# Detect file type category
file_type = self._detect_file_type(file_path)
structure.file_type = file_type
# Detect common YAML-based frameworks/configs
try:
if file_path.suffix.lower() in [".yaml", ".yml"]:
# Initialize modules collection if not present
if not hasattr(structure, "modules"):
structure.modules = []
if self._is_docker_compose_file(file_path, content):
structure.framework = "docker-compose"
for svc in self._extract_compose_services(content):
structure.modules.append({"type": "service", **svc})
elif self._looks_like_kubernetes_yaml(content):
structure.framework = "kubernetes"
for res in self._extract_k8s_resources(content):
structure.modules.append({"type": "resource", **res})
else:
# Helm/Kustomize/GitHub Actions quick hints
name = file_path.name.lower()
if name == "chart.yaml":
structure.framework = "helm"
elif name == "values.yaml":
structure.framework = getattr(structure, "framework", None) or "helm"
elif name == "kustomization.yaml":
structure.framework = "kustomize"
elif ".github" in str(file_path).replace("\\", "/") and "/workflows/" in str(
file_path
).replace("\\", "/"):
structure.framework = "github-actions"
except Exception:
# Never fail generic structure on heuristics
pass
# Extract functions (various patterns)
function_patterns = [
r"^(?:async\s+)?(?:function|def|func|sub|proc)\s+(\w+)",
r"^(\w+)\s*\(\)\s*\{",
r"^(\w+)\s*:\s*function",
r"^(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*=>",
]
for pattern in function_patterns:
for match in re.finditer(pattern, content, re.MULTILINE):
func_name = match.group(1)
structure.functions.append(
FunctionInfo(name=func_name, line=content[: match.start()].count("\n") + 1)
)
# Extract classes/types
class_patterns = [
r"^(?:export\s+)?(?:class|struct|type|interface|enum)\s+(\w+)",
r"^(\w+)\s*=\s*class\s*\{",
]
for pattern in class_patterns:
for match in re.finditer(pattern, content, re.MULTILINE):
class_name = match.group(1)
structure.classes.append(
ClassInfo(name=class_name, line=content[: match.start()].count("\n") + 1)
)
# Extract sections (markdown headers, etc.)
if file_type in ["markdown", "documentation", "markup"]:
section_pattern = r"^(#{1,6})\s+(.+)$"
for match in re.finditer(section_pattern, content, re.MULTILINE):
level = len(match.group(1))
title = match.group(2)
structure.sections.append(
{
"title": title,
"level": level,
"line": content[: match.start()].count("\n") + 1,
}
)
# Extract variables/constants
var_patterns = [
r"^(?:const|let|var|val)\s+(\w+)",
r"^(\w+)\s*[:=]\s*[^=]",
r"^export\s+(\w+)",
]
for pattern in var_patterns:
for match in re.finditer(pattern, content, re.MULTILINE):
var_name = match.group(1)
structure.variables.append(
{
"name": var_name,
"line": content[: match.start()].count("\n") + 1,
"type": "variable",
}
)
# Detect constants (UPPERCASE variables)
const_pattern = r"^([A-Z][A-Z0-9_]*)\s*[:=]"
for match in re.finditer(const_pattern, content, re.MULTILINE):
structure.constants.append(match.group(1))
# Extract TODO/FIXME comments
todo_pattern = r"(?:#|//|/\*|\*)\s*(TODO|FIXME|HACK|NOTE|XXX|BUG):\s*(.+)"
for match in re.finditer(todo_pattern, content, re.IGNORECASE):
structure.todos.append(
{
"type": match.group(1).upper(),
"message": match.group(2).strip(),
"line": content[: match.start()].count("\n") + 1,
}
)
# Count blocks (based on indentation or braces)
structure.block_count = content.count("{")
structure.indent_levels = self._analyze_indentation(content)
return structure
calculate_complexity¶
Calculate basic complexity metrics for generic text.
Provides simplified complexity estimation based on: - Line count and length - Nesting depth (indentation/braces) - Decision keywords - File type specific metrics
| PARAMETER | DESCRIPTION |
|---|---|
content | File content TYPE: |
file_path | Path to the file being analyzed TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
ComplexityMetrics | ComplexityMetrics object with basic metrics |
Source code in tenets/core/analysis/implementations/generic_analyzer.py
def calculate_complexity(self, content: str, file_path: Path) -> ComplexityMetrics:
"""Calculate basic complexity metrics for generic text.
Provides simplified complexity estimation based on:
- Line count and length
- Nesting depth (indentation/braces)
- Decision keywords
- File type specific metrics
Args:
content: File content
file_path: Path to the file being analyzed
Returns:
ComplexityMetrics object with basic metrics
"""
metrics = ComplexityMetrics()
# Basic line metrics
lines = content.split("\n")
# Trim leading/trailing empty lines for line count to match human expectations/tests
start = 0
end = len(lines)
while start < end and lines[start].strip() == "":
start += 1
while end > start and lines[end - 1].strip() == "":
end -= 1
trimmed_lines = lines[start:end]
# Preserve historical/test expectation: an entirely empty file counts as 1 line (logical line),
# while code_lines will be 0. Non-empty (after trimming) counts actual trimmed lines.
if not trimmed_lines:
metrics.line_count = 1
else:
metrics.line_count = len(trimmed_lines)
# Character count: count characters, and if file doesn't end with newline, count implicit final EOL
metrics.character_count = len(content) + (0 if content.endswith("\n") else 1)
# Count comment lines (generic patterns)
comment_patterns = [
r"^\s*#", # Hash comments
r"^\s*//", # Double slash comments
r"^\s*/\*", # Block comment start
r"^\s*\*", # Block comment continuation
r"^\s*<!--", # HTML/XML comments
r"^\s*;", # Semicolon comments (INI, assembly)
r"^\s*--", # SQL/Lua comments
r"^\s*%", # LaTeX/MATLAB comments
]
comment_lines = 0
for line in trimmed_lines:
if any(re.match(pattern, line) for pattern in comment_patterns):
comment_lines += 1
# Compute code lines as total lines minus comment lines (consistent with tests)
# For empty file (line_count==1 but no trimmed lines), code_lines should be 0
if not trimmed_lines:
metrics.code_lines = 0
else:
metrics.code_lines = metrics.line_count - comment_lines
metrics.comment_lines = comment_lines
metrics.comment_ratio = comment_lines / metrics.line_count if metrics.line_count > 0 else 0
# Estimate cyclomatic complexity (decision points)
decision_keywords = [
r"\bif\b",
r"\belse\b",
r"\belif\b",
r"\belsif\b",
r"\bfor\b",
r"\bwhile\b",
r"\bdo\b",
r"\bcase\b",
r"\bwhen\b",
r"\btry\b",
r"\bcatch\b",
r"\bexcept\b",
r"\bunless\b",
r"\buntil\b",
r"\bswitch\b",
r"\b\?\s*[^:]+\s*:",
r"\|\|",
r"&&",
r"\band\b",
r"\bor\b",
]
complexity = 1 # Base complexity
for keyword in decision_keywords:
complexity += len(re.findall(keyword, content, re.IGNORECASE))
metrics.cyclomatic = min(complexity, 50) # Cap at 50 for generic files
# Estimate nesting depth
max_depth = 0
current_depth = 0
for line in lines:
# Track braces
current_depth += line.count("{") - line.count("}")
current_depth += line.count("(") - line.count(")")
current_depth += line.count("[") - line.count("]")
max_depth = max(max_depth, current_depth)
# Reset if negative (mismatched brackets)
current_depth = max(current_depth, 0)
# Also check indentation depth
indent_depth = self._calculate_max_indent(lines)
# Combine and cap at 10
metrics.max_depth = min(max(max_depth, indent_depth), 10)
# File type specific metrics
file_type = self._detect_file_type(file_path)
if file_type == "configuration":
# For config files, count keys/sections
metrics.key_count = len(re.findall(r"^\s*[\w\-\.]+\s*[:=]", content, re.MULTILINE))
metrics.section_count = len(re.findall(r"^\s*\[[\w\-\.]+\]", content, re.MULTILINE))
elif file_type == "markup":
# For markup files, count tags
metrics.tag_count = len(re.findall(r"<\w+", content))
metrics.header_count = len(re.findall(r"^#{1,6}\s+", content, re.MULTILINE))
elif file_type == "data":
# For data files, estimate structure
if file_path.suffix.lower() == ".csv":
lines_sample = lines[:10] if len(lines) > 10 else lines
if lines_sample:
# Estimate columns
metrics.column_count = len(lines_sample[0].split(","))
metrics.row_count = len(lines) - 1 # Exclude header
# Calculate a simple maintainability index
if metrics.code_lines > 0:
# Simplified calculation
maintainability = 100
# Penalize high complexity
maintainability -= min(30, complexity * 0.5)
# Penalize deep nesting
maintainability -= min(20, metrics.max_depth * 2)
# Reward comments
maintainability += min(10, metrics.comment_ratio * 30)
# Penalize very long files
if metrics.line_count > 1000:
maintainability -= 10
elif metrics.line_count > 500:
maintainability -= 5
metrics.maintainability_index = max(0, min(100, maintainability))
return metrics
extract_context_relevant_sections¶
extract_context_relevant_sections(content: str, file_path: Path, prompt_keywords: List[str], search_depth: int = 2, min_confidence: float = 0.6, max_sections: int = 10) -> Dict[str, Any]
Extract sections of documentation that reference prompt keywords/concepts.
This method identifies and extracts the most relevant parts of documentation files based on direct references and semantic similarity to prompt keywords.
| PARAMETER | DESCRIPTION |
|---|---|
content | File content TYPE: |
file_path | Path to the file being analyzed TYPE: |
prompt_keywords | Keywords/phrases from the user's prompt |
search_depth | How deep to search (1=direct, 2=semantic, 3=deep analysis) TYPE: |
min_confidence | Minimum confidence threshold for relevance (0.0-1.0) TYPE: |
max_sections | Maximum number of contextual sections to preserve TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
Dict[str, Any] | Dictionary containing relevant sections with metadata |
Source code in tenets/core/analysis/implementations/generic_analyzer.py
def extract_context_relevant_sections(
self,
content: str,
file_path: Path,
prompt_keywords: List[str],
search_depth: int = 2,
min_confidence: float = 0.6,
max_sections: int = 10,
) -> Dict[str, Any]:
"""Extract sections of documentation that reference prompt keywords/concepts.
This method identifies and extracts the most relevant parts of documentation
files based on direct references and semantic similarity to prompt keywords.
Args:
content: File content
file_path: Path to the file being analyzed
prompt_keywords: Keywords/phrases from the user's prompt
search_depth: How deep to search (1=direct, 2=semantic, 3=deep analysis)
min_confidence: Minimum confidence threshold for relevance (0.0-1.0)
max_sections: Maximum number of contextual sections to preserve
Returns:
Dictionary containing relevant sections with metadata
"""
if not prompt_keywords:
return {
"relevant_sections": [],
"metadata": {"total_sections": 0, "matched_sections": 0},
}
file_type = self._detect_file_type(file_path)
# Extract sections based on file type
sections = self._extract_document_sections(content, file_path, file_type)
# Score sections based on relevance to prompt keywords
scored_sections = []
for section in sections:
score, matches = self._calculate_section_relevance(
section, prompt_keywords, search_depth
)
if score >= min_confidence:
scored_sections.append(
{
**section,
"relevance_score": score,
"keyword_matches": matches,
"context_type": self._determine_context_type(section, matches),
}
)
# Sort by relevance and limit to max_sections
scored_sections.sort(key=lambda x: x["relevance_score"], reverse=True)
relevant_sections = scored_sections[:max_sections]
# Extract code examples and references within relevant sections
for section in relevant_sections:
section["code_examples"] = self._extract_code_examples_from_section(section)
section["api_references"] = self._extract_api_references_from_section(section)
section["config_references"] = self._extract_config_references_from_section(section)
metadata = {
"total_sections": len(sections),
"matched_sections": len(scored_sections),
"relevant_sections": len(relevant_sections),
"file_type": file_type,
"search_depth": search_depth,
"min_confidence": min_confidence,
"avg_relevance_score": (
sum(s["relevance_score"] for s in relevant_sections) / len(relevant_sections)
if relevant_sections
else 0.0
),
}
return {"relevant_sections": relevant_sections, "metadata": metadata}
GoAnalyzer¶
Bases: LanguageAnalyzer
Go code analyzer.
Provides comprehensive analysis for Go files including: - Import analysis with vendored and internal imports - Function, method and interface extraction - Struct analysis with embedded types - Goroutine and channel detection - Error handling patterns - Defer statement tracking - Package-level analysis - Go module support
Go's export mechanism is based on capitalization - identifiers starting with uppercase letters are exported.
Initialize the Go analyzer with logger.
Source code in tenets/core/analysis/implementations/go_analyzer.py
extract_imports¶
Extract imports from Go code.
Handles: - Single imports: import "fmt" - Grouped imports: import ( "fmt" "strings" ) - Aliased imports: import f "fmt" - Dot imports: import . "fmt" - Blank imports: import _ "database/sql" - Vendored imports - Internal packages
| PARAMETER | DESCRIPTION |
|---|---|
content | Go source code TYPE: |
file_path | Path to the file being analyzed TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
List[ImportInfo] | List of ImportInfo objects with import details |
Source code in tenets/core/analysis/implementations/go_analyzer.py
def extract_imports(self, content: str, file_path: Path) -> List[ImportInfo]:
"""Extract imports from Go code.
Handles:
- Single imports: import "fmt"
- Grouped imports: import ( "fmt" "strings" )
- Aliased imports: import f "fmt"
- Dot imports: import . "fmt"
- Blank imports: import _ "database/sql"
- Vendored imports
- Internal packages
Args:
content: Go source code
file_path: Path to the file being analyzed
Returns:
List of ImportInfo objects with import details
"""
imports = []
lines = content.split("\n")
import_block = False
import_block_start = 0
for i, line in enumerate(lines, 1):
# Skip comments
if line.strip().startswith("//"):
continue
# Single import statement
single_import = re.match(r'^\s*import\s+"([^"]+)"', line)
if single_import:
imports.append(
ImportInfo(
module=single_import.group(1),
line=i,
type="import",
is_relative=False,
is_vendored=self._is_vendored_import(single_import.group(1)),
is_internal="internal" in single_import.group(1),
)
)
continue
# Aliased single import
aliased_import = re.match(r'^\s*import\s+(\w+)\s+"([^"]+)"', line)
if aliased_import:
imports.append(
ImportInfo(
module=aliased_import.group(2),
alias=aliased_import.group(1),
line=i,
type="aliased",
is_relative=False,
is_vendored=self._is_vendored_import(aliased_import.group(2)),
)
)
continue
# Dot import
dot_import = re.match(r'^\s*import\s+\.\s+"([^"]+)"', line)
if dot_import:
imports.append(
ImportInfo(
module=dot_import.group(1),
alias=".",
line=i,
type="dot_import",
is_relative=False,
)
)
continue
# Blank import
blank_import = re.match(r'^\s*import\s+_\s+"([^"]+)"', line)
if blank_import:
imports.append(
ImportInfo(
module=blank_import.group(1),
alias="_",
line=i,
type="blank_import",
is_relative=False,
purpose="side_effects",
)
)
continue
# Import block start
if re.match(r"^\s*import\s*\(", line):
import_block = True
import_block_start = i
continue
# Inside import block
if import_block:
# Check for end of import block
if ")" in line:
import_block = False
continue
# Standard import in block
standard_import = re.match(r'^\s*"([^"]+)"', line)
if standard_import:
module = standard_import.group(1)
imports.append(
ImportInfo(
module=module,
line=i,
type="import",
is_relative=False,
is_vendored=self._is_vendored_import(module),
is_internal="internal" in module,
is_stdlib=self._is_stdlib_import(module),
)
)
continue
# Aliased import in block
aliased_import = re.match(r'^\s*(\w+)\s+"([^"]+)"', line)
if aliased_import:
module = aliased_import.group(2)
imports.append(
ImportInfo(
module=module,
alias=aliased_import.group(1),
line=i,
type="aliased",
is_relative=False,
is_vendored=self._is_vendored_import(module),
)
)
continue
# Dot import in block
dot_import = re.match(r'^\s*\.\s+"([^"]+)"', line)
if dot_import:
imports.append(
ImportInfo(
module=dot_import.group(1),
alias=".",
line=i,
type="dot_import",
is_relative=False,
)
)
continue
# Blank import in block
blank_import = re.match(r'^\s*_\s+"([^"]+)"', line)
if blank_import:
imports.append(
ImportInfo(
module=blank_import.group(1),
alias="_",
line=i,
type="blank_import",
is_relative=False,
purpose="side_effects",
)
)
# Categorize imports
self._categorize_imports(imports)
return imports
extract_exports¶
Extract exported symbols from Go code.
In Go, exported identifiers start with an uppercase letter. This includes functions, types, constants, and variables.
| PARAMETER | DESCRIPTION |
|---|---|
content | Go source code TYPE: |
file_path | Path to the file being analyzed TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
List[Dict[str, Any]] | List of exported symbols with metadata |
Source code in tenets/core/analysis/implementations/go_analyzer.py
def extract_exports(self, content: str, file_path: Path) -> List[Dict[str, Any]]:
"""Extract exported symbols from Go code.
In Go, exported identifiers start with an uppercase letter.
This includes functions, types, constants, and variables.
Args:
content: Go source code
file_path: Path to the file being analyzed
Returns:
List of exported symbols with metadata
"""
exports = []
# Extract package name
package_match = re.search(r"^\s*package\s+(\w+)", content, re.MULTILINE)
package_name = package_match.group(1) if package_match else "unknown"
# Exported functions
func_pattern = (
r"^func\s+([A-Z][a-zA-Z0-9]*)\s*\(([^)]*)\)(?:\s*\(([^)]*)\))?(?:\s*([^{]+))?\s*\{"
)
for match in re.finditer(func_pattern, content, re.MULTILINE):
func_name = match.group(1)
params = match.group(2)
return_params = match.group(3)
return_type = match.group(4)
exports.append(
{
"name": func_name,
"type": "function",
"line": content[: match.start()].count("\n") + 1,
"package": package_name,
"signature": self._build_function_signature(
func_name, params, return_params, return_type
),
"has_receiver": False,
}
)
# Exported methods (with receivers)
method_pattern = r"^func\s+\(([^)]+)\)\s+([A-Z][a-zA-Z0-9]*)\s*\(([^)]*)\)(?:\s*\(([^)]*)\))?(?:\s*([^{]+))?\s*\{"
for match in re.finditer(method_pattern, content, re.MULTILINE):
receiver = match.group(1)
method_name = match.group(2)
params = match.group(3)
return_params = match.group(4)
return_type = match.group(5)
# Parse receiver type
receiver_type = self._parse_receiver(receiver)
exports.append(
{
"name": method_name,
"type": "method",
"line": content[: match.start()].count("\n") + 1,
"receiver": receiver_type,
"package": package_name,
"signature": self._build_method_signature(
receiver, method_name, params, return_params, return_type
),
"has_receiver": True,
}
)
# Exported types (structs, interfaces, type aliases)
type_pattern = r"^type\s+([A-Z][a-zA-Z0-9]*)\s+(.+?)(?:\n|\{)"
for match in re.finditer(type_pattern, content, re.MULTILINE):
type_name = match.group(1)
type_def = match.group(2).strip()
# Determine type kind
if "struct" in type_def:
type_kind = "struct"
elif "interface" in type_def:
type_kind = "interface"
elif "=" in type_def:
type_kind = "alias"
else:
type_kind = "type"
exports.append(
{
"name": type_name,
"type": type_kind,
"line": content[: match.start()].count("\n") + 1,
"package": package_name,
"definition": type_def[:50] if len(type_def) > 50 else type_def,
}
)
# Exported constants
const_pattern = r"^const\s+([A-Z][a-zA-Z0-9]*)\s*(?:[\w\s]+)?\s*="
for match in re.finditer(const_pattern, content, re.MULTILINE):
exports.append(
{
"name": match.group(1),
"type": "constant",
"line": content[: match.start()].count("\n") + 1,
"package": package_name,
}
)
# Exported constant blocks
const_block_pattern = r"^const\s*\((.*?)\)"
for match in re.finditer(const_block_pattern, content, re.MULTILINE | re.DOTALL):
block_content = match.group(1)
for const_match in re.finditer(r"^\s*([A-Z][a-zA-Z0-9]*)", block_content, re.MULTILINE):
exports.append(
{
"name": const_match.group(1),
"type": "constant",
"line": content[: match.start()].count("\n")
+ block_content[: const_match.start()].count("\n")
+ 1,
"package": package_name,
"in_block": True,
}
)
# Exported variables
var_pattern = r"^var\s+([A-Z][a-zA-Z0-9]*)\s+"
for match in re.finditer(var_pattern, content, re.MULTILINE):
exports.append(
{
"name": match.group(1),
"type": "variable",
"line": content[: match.start()].count("\n") + 1,
"package": package_name,
}
)
return exports
extract_structure¶
Extract code structure from Go file.
Extracts: - Package declaration - Functions and methods - Structs (treated as classes) - Interfaces - Type aliases - Constants and variables - Goroutines and channels - Init functions
| PARAMETER | DESCRIPTION |
|---|---|
content | Go source code TYPE: |
file_path | Path to the file being analyzed TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
CodeStructure | CodeStructure object with extracted elements |
Source code in tenets/core/analysis/implementations/go_analyzer.py
def extract_structure(self, content: str, file_path: Path) -> CodeStructure:
"""Extract code structure from Go file.
Extracts:
- Package declaration
- Functions and methods
- Structs (treated as classes)
- Interfaces
- Type aliases
- Constants and variables
- Goroutines and channels
- Init functions
Args:
content: Go source code
file_path: Path to the file being analyzed
Returns:
CodeStructure object with extracted elements
"""
structure = CodeStructure()
# Extract package name
package_match = re.search(r"^\s*package\s+(\w+)", content, re.MULTILINE)
if package_match:
structure.package = package_match.group(1)
structure.is_main = structure.package == "main"
# Extract functions
func_pattern = (
r"^func\s+(?:\([^)]+\)\s+)?(\w+)\s*\(([^)]*)\)(?:\s*\(([^)]*)\))?(?:\s*([^{]+))?"
)
for match in re.finditer(func_pattern, content, re.MULTILINE):
func_name = match.group(1)
params = match.group(2)
# Check for special functions
is_init = func_name == "init"
is_main = func_name == "main" and structure.is_main
is_test = func_name.startswith("Test") or func_name.startswith("Benchmark")
func_info = FunctionInfo(
name=func_name,
line=content[: match.start()].count("\n") + 1,
args=self._parse_go_params(params),
is_exported=func_name[0].isupper(),
is_init=is_init,
is_main=is_main,
is_test=is_test,
)
structure.functions.append(func_info)
# Extract structs (as classes)
struct_pattern = r"^type\s+(\w+)\s+struct\s*\{"
for match in re.finditer(struct_pattern, content, re.MULTILINE):
struct_name = match.group(1)
# Find struct fields
struct_start = match.end()
brace_count = 1
struct_end = struct_start
for i, char in enumerate(content[struct_start:], struct_start):
if char == "{":
brace_count += 1
elif char == "}":
brace_count -= 1
if brace_count == 0:
struct_end = i
break
struct_content = content[struct_start:struct_end]
fields = self._extract_struct_fields(struct_content)
# Find methods for this struct
methods = self._find_struct_methods(content, struct_name)
class_info = ClassInfo(
name=struct_name,
line=content[: match.start()].count("\n") + 1,
is_exported=struct_name[0].isupper(),
fields=fields,
methods=methods,
embedded_types=self._find_embedded_types(struct_content),
)
structure.classes.append(class_info)
# Extract interfaces
interface_pattern = r"^type\s+(\w+)\s+interface\s*\{"
for match in re.finditer(interface_pattern, content, re.MULTILINE):
interface_name = match.group(1)
# Find interface methods
interface_start = match.end()
brace_count = 1
interface_end = interface_start
for i, char in enumerate(content[interface_start:], interface_start):
if char == "{":
brace_count += 1
elif char == "}":
brace_count -= 1
if brace_count == 0:
interface_end = i
break
interface_content = content[interface_start:interface_end]
methods = self._extract_interface_methods(interface_content)
structure.interfaces.append(
{
"name": interface_name,
"line": content[: match.start()].count("\n") + 1,
"is_exported": interface_name[0].isupper(),
"methods": methods,
"is_empty": len(methods) == 0, # Empty interface (interface{})
}
)
# Extract type aliases
type_alias_pattern = r"^type\s+(\w+)\s*=\s*(.+)$"
for match in re.finditer(type_alias_pattern, content, re.MULTILINE):
structure.type_aliases.append(
{
"name": match.group(1),
"base_type": match.group(2).strip(),
"line": content[: match.start()].count("\n") + 1,
"is_exported": match.group(1)[0].isupper(),
}
)
# Extract custom type definitions
type_def_pattern = r"^type\s+(\w+)\s+(\w+)$"
for match in re.finditer(type_def_pattern, content, re.MULTILINE):
if not re.match(r"^type\s+\w+\s+(?:struct|interface)", content[match.start() :]):
structure.type_definitions.append(
{
"name": match.group(1),
"base_type": match.group(2),
"line": content[: match.start()].count("\n") + 1,
"is_exported": match.group(1)[0].isupper(),
}
)
# Extract constants
const_pattern = r"^const\s+(\w+)"
for match in re.finditer(const_pattern, content, re.MULTILINE):
const_name = match.group(1)
structure.constants.append(const_name)
structure.variables.append(
{
"name": const_name,
"line": content[: match.start()].count("\n") + 1,
"type": "constant",
"is_exported": const_name[0].isupper(),
}
)
# Extract variables
var_pattern = r"^var\s+(\w+)"
for match in re.finditer(var_pattern, content, re.MULTILINE):
var_name = match.group(1)
structure.variables.append(
{
"name": var_name,
"line": content[: match.start()].count("\n") + 1,
"type": "variable",
"is_exported": var_name[0].isupper(),
}
)
# Count goroutines
goroutine_pattern = r"\bgo\s+(?:\w+\.)*\w+\s*\("
structure.goroutines_count = len(re.findall(goroutine_pattern, content))
# Count channels
channel_pattern = r"(?:chan\s+\w+|<-chan\s+\w+|chan<-\s+\w+)"
structure.channels_count = len(re.findall(channel_pattern, content))
# Count defer statements
defer_pattern = r"\bdefer\s+"
structure.defer_count = len(re.findall(defer_pattern, content))
# Detect test file
structure.is_test_file = file_path.name.endswith("_test.go")
return structure
calculate_complexity¶
Calculate complexity metrics for Go code.
Calculates: - Cyclomatic complexity - Cognitive complexity - Error handling complexity - Concurrency complexity - Test coverage indicators
| PARAMETER | DESCRIPTION |
|---|---|
content | Go source code TYPE: |
file_path | Path to the file being analyzed TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
ComplexityMetrics | ComplexityMetrics object with calculated metrics |
Source code in tenets/core/analysis/implementations/go_analyzer.py
def calculate_complexity(self, content: str, file_path: Path) -> ComplexityMetrics:
"""Calculate complexity metrics for Go code.
Calculates:
- Cyclomatic complexity
- Cognitive complexity
- Error handling complexity
- Concurrency complexity
- Test coverage indicators
Args:
content: Go source code
file_path: Path to the file being analyzed
Returns:
ComplexityMetrics object with calculated metrics
"""
metrics = ComplexityMetrics()
# Calculate cyclomatic complexity
complexity = 1
decision_keywords = [
r"\bif\b",
r"\belse\s+if\b",
r"\belse\b",
r"\bfor\b",
r"\bswitch\b",
r"\bcase\b",
r"\bselect\b",
r"\bdefault\b",
r"\b&&\b",
r"\|\|",
]
for keyword in decision_keywords:
complexity += len(re.findall(keyword, content))
# Add complexity for range loops
complexity += len(re.findall(r"\bfor\s+\w+\s*:=\s*range\b", content))
metrics.cyclomatic = complexity
# Calculate cognitive complexity
cognitive = 0
nesting_level = 0
max_nesting = 0
lines = content.split("\n")
for line in lines:
# Skip comments
if line.strip().startswith("//"):
continue
# Track nesting
opening_braces = line.count("{")
closing_braces = line.count("}")
nesting_level += opening_braces - closing_braces
max_nesting = max(max_nesting, nesting_level)
# Control structures with nesting penalty
control_patterns = [
(r"\bif\b", 1),
(r"\bfor\b", 1),
(r"\bswitch\b", 1),
(r"\bselect\b", 2), # Higher weight for select
(r"\bcase\b", 0.5),
(r"\belse\s+if\b", 1),
(r"\belse\b", 0),
]
for pattern, weight in control_patterns:
if re.search(pattern, line):
cognitive += weight * (1 + max(0, nesting_level - 1))
# Error handling complexity
if "err != nil" in line:
cognitive += 1
metrics.error_handling_count = getattr(metrics, "error_handling_count", 0) + 1
# Panic/recover complexity
if re.search(r"\bpanic\b|\brecover\b", line):
cognitive += 2
metrics.cognitive = cognitive
metrics.max_depth = max_nesting
# Count code elements
metrics.line_count = len(lines)
metrics.code_lines = self._count_code_lines(content)
metrics.comment_lines = self._count_comment_lines(content)
metrics.comment_ratio = (
metrics.comment_lines / metrics.line_count if metrics.line_count > 0 else 0
)
# Count functions and methods
metrics.function_count = len(
re.findall(r"^func\s+(?:\([^)]+\)\s+)?\w+\s*\(", content, re.MULTILINE)
)
# Count structs and interfaces
metrics.struct_count = len(re.findall(r"^type\s+\w+\s+struct\s*\{", content, re.MULTILINE))
metrics.interface_count = len(
re.findall(r"^type\s+\w+\s+interface\s*\{", content, re.MULTILINE)
)
# Concurrency metrics
metrics.goroutines_count = len(re.findall(r"\bgo\s+\w+", content))
metrics.channels_count = len(re.findall(r"chan\s+\w+", content))
metrics.select_statements = len(re.findall(r"\bselect\s*\{", content))
metrics.mutex_usage = len(re.findall(r"sync\.(?:Mutex|RWMutex)", content))
# Error handling metrics
metrics.error_checks = len(re.findall(r"if\s+err\s*!=\s*nil", content))
metrics.error_returns = len(re.findall(r"return\s+.*err", content))
# Test metrics (if test file)
if file_path.name.endswith("_test.go"):
metrics.test_count = len(re.findall(r"^func\s+Test\w+\s*\(", content, re.MULTILINE))
metrics.benchmark_count = len(
re.findall(r"^func\s+Benchmark\w+\s*\(", content, re.MULTILINE)
)
metrics.example_count = len(
re.findall(r"^func\s+Example\w*\s*\(", content, re.MULTILINE)
)
# Calculate maintainability index
import math
if metrics.code_lines > 0:
# Adjusted for Go's error handling patterns
error_factor = max(0, 1 - (metrics.error_checks / metrics.code_lines))
mi = (
171
- 5.2 * math.log(max(1, complexity))
- 0.23 * complexity
- 16.2 * math.log(metrics.code_lines)
+ 20 * error_factor
) # Bonus for proper error handling
metrics.maintainability_index = max(0, min(100, mi))
return metrics
HTMLAnalyzer¶
Bases: LanguageAnalyzer
HTML code analyzer with modern web framework support.
Provides comprehensive analysis for HTML files including: - HTML5 semantic elements - CSS and JavaScript imports - Meta tags and SEO elements - Forms and input validation - Accessibility features (ARIA, alt text, etc.) - Web components and custom elements - Framework-specific patterns (React, Vue, Angular) - Microdata and structured data - DOM complexity and nesting depth - Performance hints (lazy loading, async/defer scripts) - Security considerations (CSP, integrity checks)
Supports HTML5 and modern web development practices.
Initialize the HTML analyzer with logger.
Source code in tenets/core/analysis/implementations/html_analyzer.py
extract_imports¶
Extract external resource imports from HTML.
Handles: - tags for CSS -